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