#!/usr/bin/env python3 """ # Simple reverse plotting utility # # Copyright (C) 2014-2016 Michael Buesch # # 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, see . """ import sys if sys.version_info.major < 3: input = raw_input try: from PySide.QtCore import * from PySide.QtGui import * except ImportError as e: print("ERROR: Failed to import PySide:\n" + str(e)) print("\n===> PLEASE INSTALL PySide. <===\n") input("Press any key to abort") sys.exit(1) def equal(a, b): "Test a and b for equalness. Returns bool. Also works for float." if isinstance(a, float) or isinstance(b, float): return abs(float(a) - float(b)) < 0.00001 return a == b class Point(object): def __init__(self, x, y, lineNr=None): self.x = x self.y = y self.lineNr = lineNr def __eq__(self, other): return other and self.x == other.x and self.y == other.y def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return "Point(x=%d, y=%d, lineNr=%s)" %\ (self.x, self.y, str(self.lineNr)) class PointsEdit(QPlainTextEdit): pointsChanged = Signal(list) selectedPointChanged = Signal(Point) def __init__(self, parent=None): QPlainTextEdit.__init__(self, parent) self.setFocus() fmt = self.currentCharFormat() fmt.setFontFamily("Mono") fmt.setFontKerning(False) fmt.setFontFixedPitch(True) fmt.setFontStyleStrategy(QFont.PreferBitmap) self.setCurrentCharFormat(fmt) self.points = [] self.textChanged.connect(self.__textChanged) self.cursorPositionChanged.connect(self.__cursorPosChanged) def __parsePoint(self, line, lineNr=None): tokens = line.split(";") try: if len(tokens) != 2: raise ValueError x = int(tokens[0]) y = int(tokens[1]) except ValueError as e: return None return Point(x, y, lineNr) def __textChanged(self): self.points = [] for i, line in enumerate(self.toPlainText().splitlines()): point = self.__parsePoint(line, i) if point: self.points.append(point) self.pointsChanged.emit(self.points) def __cursorPosChanged(self): cursor = self.textCursor() cursorPos = cursor.position() text = self.toPlainText() if cursorPos >= len(text): self.selectedPointChanged.emit(None) return start = text.rfind("\n", 0, cursorPos) if start < 0: start = 0 end = text.find("\n", start + 1) if end < 0: self.selectedPointChanged.emit(None) return line = text[start : end + 1] point = self.__parsePoint(line) self.selectedPointChanged.emit(point) def addPoint(self, point): if not point: return text = self.toPlainText() newText = "" cursor = self.textCursor() newText += "%d;%d" % (point.x, point.y) if cursor.position() > 0 and\ text[cursor.position() - 1] != "\n": # We are not at the start of a line. # Move cursor to end of line. while not cursor.atEnd() and\ text[cursor.position()] != "\n": cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor) self.setTextCursor(cursor) newText = "\n" + newText if cursor.atEnd() or text[cursor.position()] != "\n": # We are the the end of the text or at the start of a line. newText = newText + "\n" cursor.insertText(newText) def removePoint(self, point): if not point: return cursor = self.textCursor() cursor.movePosition(QTextCursor.Start, QTextCursor.MoveAnchor) text = self.toPlainText() while not cursor.atEnd(): start = cursor.position() cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.MoveAnchor) end = cursor.position() if point == self.__parsePoint(text[start:end]): # Found it. Remove the point. cursor.select(QTextCursor.LineUnderCursor) cursor.removeSelectedText() # Remove the line cursor.deleteChar() # Remove the line break cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.MoveAnchor) self.setTextCursor(cursor) break cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor) def moveCursorToPoint(self, point): if not point: return cursor = self.textCursor() cursor.movePosition(QTextCursor.Start, QTextCursor.MoveAnchor) text = self.toPlainText() while not cursor.atEnd(): start = cursor.position() cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.MoveAnchor) end = cursor.position() if point == self.__parsePoint(text[start:end]): # Found it. Move the cursor here. self.setTextCursor(cursor) break cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor) class PointsEditContainer(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.setContentsMargins(QMargins(0, 0, 0, 0)) self.setLayout(QGridLayout()) label = QLabel("Points:") self.layout().addWidget(label, 0, 0) self.editWidget = PointsEdit(self) self.layout().addWidget(self.editWidget, 1, 0) class TimeTraceValueWidget(QPlainTextEdit): def __init__(self, parent=None): QPlainTextEdit.__init__(self, parent) self.setReadOnly(True) fmt = self.currentCharFormat() fmt.setFontFamily("Mono") fmt.setFontKerning(False) fmt.setFontFixedPitch(True) fmt.setFontStyleStrategy(QFont.PreferBitmap) self.setCurrentCharFormat(fmt) self.scale = 1.0 self.trace = [] def setScale(self, scale): self.scale = scale self.setTrace(self.trace) def __makeLine(self, y1, y2): if equal(y1, y2): return "%.2f" % (float(y1) * self.scale) return "%.2f;%.2f" % (float(y1) * self.scale,\ float(y2) * self.scale) def setTrace(self, trace): self.trace = trace text = [] skipNext = False if trace and trace[0].x != 0: self.setPlainText("ERROR: Start not at X=0") return for i, point in enumerate(trace): if skipNext: skipNext = False continue x, y = point.x, point.y if i > 0 and trace[i - 1].x < x - 1: # Interpolate xSteps = x - trace[i - 1].x ySteps = y - trace[i - 1].y yPerX = ySteps / xSteps interY = trace[i - 1].y + yPerX for j in range(xSteps - 1): text.append(self.__makeLine(interY, interY)) interY += yPerX if i + 1 < len(trace) and x > trace[i + 1].x: text.append("ERROR: time travel") break elif i + 1 < len(trace) and x == trace[i + 1].x: # This X equals next X (this is an edge) if i + 2 < len(trace) and x == trace[i + 2].x: text.append("ERROR: More than 2 points on one X-value") break if i + 2 < len(trace) and x > trace[i + 2].x: text.append("ERROR: time travel") break text.append(self.__makeLine(y, trace[i + 1].y)) skipNext = True else: text.append(self.__makeLine(y, y)) text = "\n".join(text) text = text.replace(".", QLocale.system().decimalPoint()) self.setPlainText(text) class TimeTraceContainer(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.setContentsMargins(QMargins(0, 0, 0, 0)) self.setLayout(QGridLayout()) label = QLabel("Time trace:") self.layout().addWidget(label, 0, 0) self.valueWidget = TimeTraceValueWidget(self) self.layout().addWidget(self.valueWidget, 1, 0) self.scaleSpin = QDoubleSpinBox(self) self.scaleSpin.setDecimals(3) self.scaleSpin.setPrefix("Y-Scale: ") self.scaleSpin.setMaximum(1000.0) self.scaleSpin.setMinimum(-self.scaleSpin.maximum()) self.scaleSpin.setSingleStep(0.1) self.scaleSpin.setValue(1.0) self.layout().addWidget(self.scaleSpin, 2, 0) self.copyButton = QPushButton("Copy plain-text", self) self.layout().addWidget(self.copyButton, 3, 0) self.scaleSpin.valueChanged.connect(self.__scaleChanged) self.copyButton.released.connect(self.__copyToClipboard) def __copyToClipboard(self): clipboard = QApplication.clipboard() clipboard.setText(self.valueWidget.toPlainText()) def __scaleChanged(self): self.valueWidget.setScale(self.scaleSpin.value()) class PlotWidget(QWidget): mousePressLeft = Signal(Point) mousePressRight = Signal(Point) mousePressMiddle = Signal(Point) def __init__(self, parent=None): QWidget.__init__(self, parent) self.nrXDivs = 64 self.nrYDivs = 30 self.xOffset = 25 self.bgColor = QColor("#FFFFFF") self.gridColor = QColor("#000000") self.traceColor = QColor("#0000FF") self.pointColor = QColor("#0000FF") self.hPointColor = QColor("#FF0000") self.points = [] self.highlightedPoint = None def setNrDivs(self, nrX, nrY): self.nrXDivs = nrX self.nrYDivs = nrY self.repaint() def setPlot(self, points): self.points = points self.repaint() def highlightPoint(self, point): self.highlightedPoint = point self.repaint() def paintEvent(self, event=None): size = self.size() p = QPainter(self) # Draw background p.fillRect(self.rect(), QBrush(self.bgColor)) p.translate(self.xOffset, 0) font = p.font() font.setFamily("Mono") font.setPointSize(8) p.setFont(font) # Draw the grid pen = QPen(self.gridColor) pen.setWidth(1) p.setPen(pen) # vertical lines for i in range(self.nrXDivs + 1): x = int(round(i * ((float(size.width()) - self.xOffset) / self.nrXDivs))) p.drawLine(x, 0, x, size.height()) # horizontal lines count = self.nrYDivs // 2 for i in range(self.nrYDivs + 1): y = int(round(i * (float(size.height()) / self.nrYDivs))) p.drawLine(0, y, (size.width() - self.xOffset), y) yDivPix = size.height() // self.nrYDivs p.drawText(-self.xOffset, y - yDivPix // 2, self.xOffset - 3, yDivPix, Qt.AlignRight | Qt.AlignVCenter, "%d" % count) count -= 1 # vertical center line pen.setWidth(3) p.setPen(pen) p.drawLine(0, 0, 0, size.height()) # horizontal center line pen.setWidth(3) p.setPen(pen) p.drawLine(0, size.height() // 2, size.width() - self.xOffset, size.height() // 2) if self.points: # Draw the trace path = QPainterPath() x, y = self.__pointToCoord(self.points[0]) path.moveTo(x, y) for point in self.points[1:]: x, y = self.__pointToCoord(point) path.lineTo(x, y) pen = QPen(self.traceColor) pen.setWidth(3) p.setPen(pen) p.drawPath(path) # Draw the points for point in self.points: x, y = self.__pointToCoord(point) if point == self.highlightedPoint: pen = QPen(self.hPointColor) brush = QBrush(self.hPointColor) else: pen = QPen(self.pointColor) brush = QBrush(self.pointColor) pen.setWidth(1) p.setPen(pen) p.setBrush(brush) p.drawEllipse(x - 3, y - 3, 6, 6) def __pointToCoord(self, point): x, y = 0, float(self.size().height()) / 2.0 x += point.x * ((float(self.size().width()) - self.xOffset) / self.nrXDivs) y -= point.y * (float(self.size().height()) / self.nrYDivs) return int(round(x)), int(round(y)) def mousePressEvent(self, event): x, y = float(event.x()), float(event.y()) x -= self.xOffset # Center Y y -= float(self.size().height()) / 2.0 y *= -1.0 # To divs x /= (float(self.size().width()) - self.xOffset) / self.nrXDivs y /= float(self.size().height()) / self.nrYDivs x, y = int(round(x)), int(round(y)) if x >= 0: point = Point(x, y) if event.button() == Qt.LeftButton: self.mousePressLeft.emit(point) elif event.button() == Qt.RightButton: self.mousePressRight.emit(point) elif event.button() == Qt.MiddleButton: self.mousePressMiddle.emit(point) class PlotContainer(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.setContentsMargins(QMargins(0, 0, 0, 0)) self.setLayout(QGridLayout()) hLayout = QHBoxLayout() self.xSizeSpin = QSpinBox(self) self.xSizeSpin.setPrefix("X size: ") self.xSizeSpin.setSuffix(" divs") self.xSizeSpin.setMinimum(2) self.xSizeSpin.setMaximum(4096) self.xSizeSpin.setSingleStep(2) hLayout.addWidget(self.xSizeSpin) self.ySizeSpin = QSpinBox(self) self.ySizeSpin.setPrefix("Y size: ") self.ySizeSpin.setSuffix(" divs") self.ySizeSpin.setMinimum(2) self.ySizeSpin.setMaximum(4096) self.ySizeSpin.setSingleStep(2) hLayout.addWidget(self.ySizeSpin) hLayout.addStretch() self.layout().addLayout(hLayout, 0, 0) self.plot = PlotWidget(self) self.layout().addWidget(self.plot, 1, 0) self.xSizeSpin.setValue(self.plot.nrXDivs) self.ySizeSpin.setValue(self.plot.nrYDivs) self.xSizeSpin.valueChanged.connect(self.__divsChanged) self.ySizeSpin.valueChanged.connect(self.__divsChanged) def __divsChanged(self): self.plot.setNrDivs(self.xSizeSpin.value(), self.ySizeSpin.value()) class MainSplitter(QSplitter): def __init__(self, parent=None): QSplitter.__init__(self, parent) self.plot = PlotContainer(self) self.addWidget(self.plot) self.pointsEdit = PointsEditContainer(self) self.addWidget(self.pointsEdit) self.traceValues = TimeTraceContainer(self) self.addWidget(self.traceValues) self.pointsEdit.editWidget.pointsChanged.connect(self.plot.plot.setPlot) self.pointsEdit.editWidget.pointsChanged.connect(self.traceValues.valueWidget.setTrace) self.pointsEdit.editWidget.selectedPointChanged.connect(self.plot.plot.highlightPoint) self.plot.plot.mousePressLeft.connect(self.pointsEdit.editWidget.addPoint) self.plot.plot.mousePressRight.connect(self.pointsEdit.editWidget.removePoint) self.plot.plot.mousePressMiddle.connect(self.pointsEdit.editWidget.moveCursorToPoint) self.setSizes([800, 100, 100]) class MainWidget(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.setLayout(QGridLayout()) self.splitter = MainSplitter(self) self.layout().addWidget(self.splitter, 0, 0) class MainWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.setWindowTitle("ManPlot") self.setCentralWidget(MainWidget(self)) self.resize(1100, 500) def main(): app = QApplication(sys.argv) mainwnd = MainWindow() mainwnd.show() return app.exec_() if __name__ == "__main__": sys.exit(main())