1#  NanoVNASaver
2#
3#  A python program to view and export Touchstone data from a NanoVNA
4#  Copyright (C) 2019, 2020  Rune B. Broberg
5#  Copyright (C) 2020 NanoVNA-Saver Authors
6#
7#  This program is free software: you can redistribute it and/or modify
8#  it under the terms of the GNU General Public License as published by
9#  the Free Software Foundation, either version 3 of the License, or
10#  (at your option) any later version.
11#
12#  This program is distributed in the hope that it will be useful,
13#  but WITHOUT ANY WARRANTY; without even the implied warranty of
14#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#  GNU General Public License for more details.
16#
17#  You should have received a copy of the GNU General Public License
18#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
19import math
20from typing import List, Set
21import logging
22
23from PyQt5 import QtWidgets, QtGui, QtCore
24from PyQt5.QtCore import pyqtSignal
25
26from NanoVNASaver.RFTools import Datapoint
27from NanoVNASaver.Marker import Marker
28logger = logging.getLogger(__name__)
29
30
31class Chart(QtWidgets.QWidget):
32    sweepColor = QtCore.Qt.darkYellow
33    secondarySweepColor = QtCore.Qt.darkMagenta
34    referenceColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.blue)
35    referenceColor.setAlpha(64)
36    secondaryReferenceColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.blue)
37    secondaryReferenceColor.setAlpha(64)
38    backgroundColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.white)
39    foregroundColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.lightGray)
40    textColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.black)
41    swrColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.red)
42    swrColor.setAlpha(128)
43    data: List[Datapoint] = []
44    reference: List[Datapoint] = []
45    markers: List[Marker] = []
46    swrMarkers: Set[float] = set()
47    bands = None
48    draggedMarker: Marker = None
49    name = ""
50    sweepTitle = ""
51    drawLines = False
52    minChartHeight = 200
53    minChartWidth = 200
54    chartWidth = minChartWidth
55    chartHeight = minChartHeight
56    lineThickness = 1
57    pointSize = 2
58    markerSize = 3
59    drawMarkerNumbers = False
60    markerAtTip = False
61    filledMarkers = False
62    draggedBox = False
63    draggedBoxStart = (0, 0)
64    draggedBoxCurrent = (-1, -1)
65    moveStartX = -1
66    moveStartY = -1
67
68    isPopout = False
69    popoutRequested = pyqtSignal(object)
70
71    def __init__(self, name):
72        super().__init__()
73        self.name = name
74
75        self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
76        self.action_save_screenshot = QtWidgets.QAction("Save image")
77        self.action_save_screenshot.triggered.connect(self.saveScreenshot)
78        self.addAction(self.action_save_screenshot)
79        self.action_popout = QtWidgets.QAction("Popout chart")
80        self.action_popout.triggered.connect(lambda: self.popoutRequested.emit(self))
81        self.addAction(self.action_popout)
82
83        self.swrMarkers = set()
84
85    def setSweepColor(self, color: QtGui.QColor):
86        self.sweepColor = color
87        self.update()
88
89    def setSecondarySweepColor(self, color: QtGui.QColor):
90        self.secondarySweepColor = color
91        self.update()
92
93    def setReferenceColor(self, color: QtGui.QColor):
94        self.referenceColor = color
95        self.update()
96
97    def setSecondaryReferenceColor(self, color: QtGui.QColor):
98        self.secondaryReferenceColor = color
99        self.update()
100
101    def setBackgroundColor(self, color: QtGui.QColor):
102        self.backgroundColor = color
103        pal = self.palette()
104        pal.setColor(QtGui.QPalette.Background, color)
105        self.setPalette(pal)
106        self.update()
107
108    def setForegroundColor(self, color: QtGui.QColor):
109        self.foregroundColor = color
110        self.update()
111
112    def setTextColor(self, color: QtGui.QColor):
113        self.textColor = color
114        self.update()
115
116    def setReference(self, data):
117        self.reference = data
118        self.update()
119
120    def resetReference(self):
121        self.reference = []
122        self.update()
123
124    def setData(self, data):
125        self.data = data
126        self.update()
127
128    def setMarkers(self, markers):
129        self.markers = markers
130
131    def setBands(self, bands):
132        self.bands = bands
133
134    def setLineThickness(self, thickness):
135        self.lineThickness = thickness
136        self.update()
137
138    def setPointSize(self, size):
139        self.pointSize = size
140        self.update()
141
142    def setMarkerSize(self, size):
143        self.markerSize = size
144        self.update()
145
146    def setSweepTitle(self, title):
147        self.sweepTitle = title
148        self.update()
149
150    def getActiveMarker(self) -> Marker:
151        if self.draggedMarker is not None:
152            return self.draggedMarker
153        for m in self.markers:
154            if m.isMouseControlledRadioButton.isChecked():
155                return m
156        return None
157
158    def getNearestMarker(self, x, y) -> Marker:
159        if len(self.data) == 0:
160            return None
161        shortest = 10**6
162        nearest = None
163        for m in self.markers:
164            mx, my = self.getPosition(self.data[m.location])
165            dx = abs(x - mx)
166            dy = abs(y - my)
167            distance = math.sqrt(dx**2 + dy**2)
168            if distance < shortest:
169                shortest = distance
170                nearest = m
171        return nearest
172
173    def getYPosition(self, d: Datapoint) -> int:
174        return 0
175
176    def getXPosition(self, d: Datapoint) -> int:
177        return 0
178
179    def getPosition(self, d: Datapoint) -> (int, int):
180        return self.getXPosition(d), self.getYPosition(d)
181
182    def setDrawLines(self, draw_lines):
183        self.drawLines = draw_lines
184        self.update()
185
186    def setDrawMarkerNumbers(self, draw_marker_numbers):
187        self.drawMarkerNumbers = draw_marker_numbers
188        self.update()
189
190    def setMarkerAtTip(self, marker_at_tip):
191        self.markerAtTip = marker_at_tip
192        self.update()
193
194    def setFilledMarkers(self, filled_markers):
195        self.filledMarkers = filled_markers
196        self.update()
197
198    @staticmethod
199    def shortenFrequency(frequency: int) -> str:
200        if frequency < 50000:
201            return str(frequency)
202        if frequency < 5000000:
203            return str(round(frequency / 1000)) + "k"
204        if frequency < 50000000:
205            return str(round(frequency / 1000000, 2)) + "M"
206        return str(round(frequency / 1000000, 1)) + "M"
207
208    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
209        if event.buttons() == QtCore.Qt.RightButton:
210            event.ignore()
211            return
212        if event.buttons() == QtCore.Qt.MiddleButton:
213            # Drag event
214            event.accept()
215            self.moveStartX = event.x()
216            self.moveStartY = event.y()
217            return
218        if event.modifiers() == QtCore.Qt.ShiftModifier:
219            self.draggedMarker = self.getNearestMarker(event.x(), event.y())
220        elif event.modifiers() == QtCore.Qt.ControlModifier:
221            event.accept()
222            self.draggedBox = True
223            self.draggedBoxStart = (event.x(), event.y())
224            return
225        self.mouseMoveEvent(event)
226
227    def mouseReleaseEvent(self, a0: QtGui.QMouseEvent) -> None:
228        self.draggedMarker = None
229        if self.draggedBox:
230            self.zoomTo(self.draggedBoxStart[0], self.draggedBoxStart[1], a0.x(), a0.y())
231            self.draggedBox = False
232            self.draggedBoxCurrent = (-1, -1)
233            self.draggedBoxStart = (0, 0)
234            self.update()
235
236    def zoomTo(self, x1, y1, x2, y2):
237        pass
238
239    def saveScreenshot(self):
240        logger.info("Saving %s to file...", self.name)
241        filename, _ = QtWidgets.QFileDialog.getSaveFileName(parent=self, caption="Save image",
242                                                            filter="PNG (*.png);;All files (*.*)")
243
244        logger.debug("Filename: %s", filename)
245        if filename != "":
246            if not QtCore.QFileInfo(filename).suffix():
247                filename += ".png"
248            self.grab().save(filename)
249
250    def copy(self):
251        new_chart = self.__class__(self.name)
252        new_chart.data = self.data
253        new_chart.reference = self.reference
254        new_chart.sweepColor = self.sweepColor
255        new_chart.secondarySweepColor = self.secondarySweepColor
256        new_chart.referenceColor = self.referenceColor
257        new_chart.secondaryReferenceColor = self.secondaryReferenceColor
258        new_chart.setBackgroundColor(self.backgroundColor)
259        new_chart.textColor = self.textColor
260        new_chart.foregroundColor = self.foregroundColor
261        new_chart.swrColor = self.swrColor
262        new_chart.markers = self.markers
263        new_chart.swrMarkers = self.swrMarkers
264        new_chart.bands = self.bands
265        new_chart.drawLines = self.drawLines
266        new_chart.markerSize = self.markerSize
267        new_chart.drawMarkerNumbers = self.drawMarkerNumbers
268        new_chart.filledMarkers = self.filledMarkers
269        new_chart.markerAtTip = self.markerAtTip
270        new_chart.resize(self.width(), self.height())
271        new_chart.setPointSize(self.pointSize)
272        new_chart.setLineThickness(self.lineThickness)
273        return new_chart
274
275    def addSWRMarker(self, swr: float):
276        self.swrMarkers.add(swr)
277        self.update()
278
279    def removeSWRMarker(self, swr: float):
280        try:
281            self.swrMarkers.remove(swr)
282        except KeyError:
283            logger.debug("KeyError from %s", self.name)
284            return
285        finally:
286            self.update()
287
288    def clearSWRMarkers(self):
289        self.swrMarkers.clear()
290        self.update()
291
292    def setSWRColor(self, color: QtGui.QColor):
293        self.swrColor = color
294        self.update()
295
296    def drawMarker(self, x, y, qp: QtGui.QPainter, color: QtGui.QColor, number=0):
297        if self.markerAtTip:
298            y -= self.markerSize
299        pen = QtGui.QPen(color)
300        qp.setPen(pen)
301        qpp = QtGui.QPainterPath()
302        qpp.moveTo(x, y + self.markerSize)
303        qpp.lineTo(x - self.markerSize, y - self.markerSize)
304        qpp.lineTo(x + self.markerSize, y - self.markerSize)
305        qpp.lineTo(x, y + self.markerSize)
306
307        if self.filledMarkers:
308            qp.fillPath(qpp, color)
309        else:
310            qp.drawPath(qpp)
311
312        if self.drawMarkerNumbers:
313            number_x = x - 3
314            number_y = y - self.markerSize - 3
315            qp.drawText(number_x, number_y, str(number))
316
317    def drawTitle(self, qp: QtGui.QPainter, position: QtCore.QPoint = None):
318        if self.sweepTitle != "":
319            qp.setPen(self.textColor)
320            if position is None:
321                qf = QtGui.QFontMetricsF(self.font())
322                width = qf.boundingRect(self.sweepTitle).width()
323                position = QtCore.QPointF(self.width()/2 - width/2, 15)
324            qp.drawText(position, self.sweepTitle)
325