Source code for tuiview.profilewindow
"""
Module that contains the ProfileDockWidget
"""
# This file is part of 'TuiView' - a simple Raster viewer
# Copyright (C) 2012 Sam Gillingham
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from PySide6.QtGui import QPen, QIcon
from PySide6.QtWidgets import QDockWidget, QWidget, QToolBar, QVBoxLayout, QLabel
from PySide6.QtWidgets import QFileDialog
from PySide6.QtGui import QAction, QPainter, QPageLayout
from PySide6.QtCore import Qt, Signal, QLocale, QPoint
from PySide6.QtPrintSupport import QPrinter
import numpy
from . import plotwidget
from .plotscalingdialog import PlotScalingDialog
[docs]class ProfileDockWidget(QDockWidget):
"""
Dockable window that is a combined profile and ruler
"""
# signals
profileClosed = Signal(QDockWidget, name='profileClosed')
"emitted when Window closed"
def __init__(self, parent, viewwidget):
QDockWidget.__init__(self, "Profile", parent)
self.viewwidget = viewwidget
self.plugins = []
# create a new widget that lives in the dock window
self.dockWidget = QWidget()
self.mainLayout = QVBoxLayout()
self.toolBar = QToolBar(self.dockWidget)
self.setupActions()
self.setupToolbar()
self.mainLayout.addWidget(self.toolBar)
self.plotWidget = plotwidget.PlotLineWidget(self)
self.mainLayout.addWidget(self.plotWidget)
self.whitePen = QPen(Qt.white)
self.whitePen.setWidth(1)
self.redPen = QPen(Qt.red)
self.redPen.setWidth(1)
self.greenPen = QPen(Qt.green)
self.greenPen.setWidth(1)
self.bluePen = QPen(Qt.blue)
self.bluePen.setWidth(1)
self.distanceLabel = QLabel()
self.mainLayout.addWidget(self.distanceLabel)
self.dockWidget.setLayout(self.mainLayout)
# tell the dock window this is the widget to display
self.setWidget(self.dockWidget)
self.resize(400, 200)
# allow plot scaling to be changed by user
# Min, Max. None means 'auto'.
self.plotScaling = (None, None)
# store the range of the data so we can init plot scale dlg
self.dataRange = None
# last polyLine so we can quickly resraw if plot scale changes
self.lastPolyLine = None
# connect if the layers have changed and we can close if our layer
# no longer exists
self.viewwidget.layers.layersChanged.connect(self.layersChanged)
[docs] def setupActions(self):
"""
Create the actions to be shown on the toolbar
"""
self.followAction = QAction(self)
self.followAction.setText("&Follow Query Tool")
self.followAction.setStatusTip("Follow Query Tool")
self.followAction.setIcon(QIcon(":/viewer/images/profileruler.png"))
self.followAction.setCheckable(True)
self.followAction.setChecked(True)
self.savePlotAction = QAction(self, triggered=self.savePlot)
self.savePlotAction.setText("&Save Plot")
self.savePlotAction.setStatusTip("Save Plot")
self.savePlotAction.setIcon(QIcon(":/viewer/images/saveplot.png"))
self.plotScalingAction = QAction(self, triggered=self.onPlotScaling)
self.plotScalingAction.setText("Set Plot Scaling")
self.plotScalingAction.setStatusTip("Set Plot Scaling")
icon = QIcon(":/viewer/images/setplotscale.png")
self.plotScalingAction.setIcon(icon)
[docs] def setupToolbar(self):
"""
Add the actions to the toolbar
"""
self.toolBar.addAction(self.followAction)
self.toolBar.addAction(self.savePlotAction)
self.toolBar.addAction(self.plotScalingAction)
[docs] def savePlot(self):
"""
Save the plot as a file. Either .pdf or .ps QPrinter
chooses format based on extension.
"""
fname, _ = QFileDialog.getSaveFileName(self, "Plot File",
filter="PDF (*.pdf);;Postscript (*.ps)")
if fname != '':
printer = QPrinter()
printer.setPageOrientation(QPageLayout.Landscape)
printer.setColorMode(QPrinter.Color)
printer.setOutputFileName(fname)
printer.setResolution(96)
painter = QPainter()
painter.begin(printer)
self.plotWidget.render(painter, QPoint())
painter.end()
[docs] def plotProfile(self, xdata, ydata, mask, pen):
"""
Plot the xdata vs ydata. Use the mask
to split the plot up so values only plotted
where mask = True
"""
# get all the indices where the mask
# changes from True to False and vice versa
changeidx = numpy.diff(mask).nonzero()[0]
# because diff() starts from the second element
# we actually want the indices relative to the start
changeidx = changeidx + 1
# go all the way to the end
changeidx = numpy.append(changeidx, mask.size)
lastidx = 0 # start at the beginning of the array
for idx in changeidx:
if mask[lastidx]:
# we are in a run of True's
xdatasub = xdata[lastidx:idx]
ydatasub = ydata[lastidx:idx]
curve = plotwidget.PlotCurve(xdatasub, ydatasub, pen)
self.plotWidget.addCurve(curve)
lastidx = idx
[docs] def newLine(self, polyLine):
"""
Widget has collected a new line
"""
if not self.followAction.isChecked():
return
title = "Profile: %s" % polyLine.layer.title
self.setWindowTitle(title)
# get the info we need out of the PolylineToolInfo
profiledata, profilemask, distance = polyLine.getProfile()
# set the distance text with commas etc as defined
# by the system locale
txt = QLocale.system().toString(distance[-1])
fmt = "Total Distance: %s" % txt
self.distanceLabel.setText(fmt)
# get rid of curves from last time
self.plotWidget.removeCurves()
if isinstance(profiledata, list):
# RGB
penList = [self.redPen, self.greenPen, self.bluePen]
minValue = None
maxValue = None
for data, pen in zip(profiledata, penList):
# record the range of the data for scaling
masked = numpy.compress(profilemask, data)
if masked.size == 0:
# can't do anything if no valid data
continue
bandMinValue = masked.min()
bandMaxValue = masked.max()
self.plotProfile(distance, data, profilemask, pen)
if minValue is None or minValue < bandMinValue:
minValue = bandMinValue
if maxValue is None or maxValue > bandMaxValue:
maxValue = bandMaxValue
else:
# greyscale
# find range of data for scaling
masked = numpy.compress(profilemask, profiledata)
if masked.size == 0:
# can't do anything if no valid data
return
minValue = masked.min()
maxValue = masked.max()
pen = self.whitePen
self.plotProfile(distance, profiledata, profilemask, pen)
self.dataRange = (minValue, maxValue)
# include total distance in case start or end off image
self.plotWidget.setXRange(0, distance[-1])
# set scaling if needed
minScale, maxScale = self.plotScaling
if minScale is None and maxScale is None:
# set back to auto
self.plotWidget.setYRange()
else:
# we need to provide both min and max so
# derive from data if needed
if minScale is None:
minScale = minValue
if maxScale is None:
maxScale = maxValue
self.plotWidget.setYRange(minScale, maxScale)
self.lastPolyLine = polyLine
[docs] def onPlotScaling(self):
"""
Allows the user to change the Y axis scaling of the plot
"""
if self.dataRange is not None:
minValue, maxValue = self.dataRange
# should inherit type of minValue etc
# which should be the same as the data array
data = numpy.array([minValue, maxValue])
else:
# uint8 default if no data
data = numpy.array([0, 1], dtype=numpy.uint8)
dlg = PlotScalingDialog(self, self.plotScaling, data)
if dlg.exec_() == PlotScalingDialog.Accepted:
self.plotScaling = dlg.getScale()
if self.lastPolyLine is not None:
self.newLine(self.lastPolyLine)
[docs] def closeEvent(self, event):
"""
Window is being closed - inform parent window
"""
self.profileClosed.emit(self)
[docs] def layersChanged(self):
"""
Layers have changed - if polyLine.layer no
longer in our list of layers then close this
window.
"""
if self.lastPolyLine is not None:
if self.lastPolyLine.layer not in self.viewwidget.layers.layers:
# remove reference
self.lastPolyLine = None
# close window
self.close()