#!/usr/bin/env python # *-----------------------------------------------------------------------* # | | # | Copyright (c) 2022 by Paul Scherrer Institute (http://www.psi.ch) | # | | # | Author Thierry Zamofing (thierry.zamofing@psi.ch) | # *-----------------------------------------------------------------------* ''' Module to handle FixTarget acquisition This contains a Widget to handle FixTargetFrames and fiducials, calculate final target positions etc. ''' #https://stackoverflow.com/questions/27909658/json-encoder-and-decoder-for-complex-numpy-arrays # import yaml # class Dice(tuple): # def __new__(cls, a, b): # return tuple.__new__(cls, [a, b]) # def __repr__(self): # return "Dice(%s,%s)" % self # d=Dice(3,6) # print(d) # print(yaml.dump(d)) # def dice_representer(dumper, data): # return dumper.represent_scalar(u'!dice', u'%sd%s' % data) # yaml.add_representer(Dice, dice_representer) # def dice_constructor(loader, node): # value = loader.construct_scalar(node) # a, b = map(int, value.split('d')) # return Dice(a, b) # yaml.add_constructor(u'!dice', dice_constructor) # print(yaml.load("initial hit points: !dice 8d4")) import logging _log=logging.getLogger(__name__) import json, base64 #, os, pickle, yaml import numpy as np import pyqtUsrObj as UsrGO import pyqtgraph as pg from PyQt5.QtCore import Qt, QFileInfo, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QTableWidgetItem, QFileDialog, QGroupBox, QWidget, QGridLayout, QTableWidget, QAbstractItemView, QItemDelegate, QVBoxLayout, QLabel, QPushButton, QApplication, QMainWindow, QHeaderView, QLineEdit, QSpinBox, QMessageBox, QAction, QComboBox, QCheckBox) class MyJsonEncoder(json.JSONEncoder): """ Special json encoder for numpy types """ def default(self, obj): if isinstance(obj, np.integer): return int(obj) elif isinstance(obj, np.floating): return float(obj) elif isinstance(obj, np.ndarray): data_b64=base64.b64encode(obj.data) return dict(__ndarray__=data_b64,dtype=str(obj.dtype),shape=obj.shape) #return obj.tolist() elif isinstance(obj, set): return list(obj) elif type(obj) not in (dict,list,str,int): try: return obj.obj2json(self) except AttributeError: _log.error('dont know how to json') return repr(obj) return json.JSONEncoder.default(self, obj) def MyJsonDecoder(dct): if isinstance(dct, dict): if '__class__' in dct: cls=dct.pop('__class__') cls=UsrGO.__dict__[cls] obj=cls.__new__(cls) obj.__init__(**dct) #try: # obj.json2obj(dct) #except AttributeError: # obj.__init__(**dct) return obj elif '__ndarray__' in dct: data = base64.b64decode(dct['__ndarray__']) return np.frombuffer(data[:-1], dct['dtype']).reshape(dct['shape']) return dct class MarkerDelegate(QItemDelegate): def createEditor(self, parent, option, index): comboBox=QLineEdit(parent) comboBox.editingFinished.connect(self.emitCommitData) return comboBox def setEditorData(self, editor, index): # pos = comboBox.findText(index.model().data(index), Qt.MatchExactly) # comboBox.setCurrentIndex(pos) editor.setText("{}".format(index.model().data(index))) def setModelData(self, editor, model, index): print("model {}".format(model)) print( "delegate setData: {}x{} => {}".format( index.row(), index.column(), editor.text() ) ) model.setData(index, editor.text()) model.setData(index, float(editor.text()), Qt.UserRole) def emitCommitData(self): print("imagedelegate emitting") self.commitData.emit(self.sender()) class WndFixTarget(QWidget): prefixSelected = pyqtSignal(str) dataFileLoaded = pyqtSignal(str) prelocatedDataUpdated = pyqtSignal() markersDeleted = pyqtSignal() markerAdded = pyqtSignal(bool, list) # emits is_fiducial(boolean), [x, y, cx, cy] selectedRowChanged = pyqtSignal(int) moveFastStageRequest = pyqtSignal(float, float) def __init__(self, parent=None,data=None): super(WndFixTarget, self).__init__(parent) self._xtals_transformed = True self._current_row = None layout = QVBoxLayout() self.setLayout(layout) frame = QWidget() bl = QGridLayout() frame.setLayout(bl) btnLd = QPushButton("Load Datafile") btnLd.clicked.connect(lambda: self.load_file(None)) bl.addWidget(btnLd, 0, 0) btnSv = QPushButton("Save Datafile") btnSv.clicked.connect(lambda: self.save_file()) bl.addWidget(btnSv, 0, 1) # but = QPushButton("Random Data") # but.clicked.connect(self.generate_random_data) # bl.addWidget(but, 4, 0) # self._num_random_points = QSpinBox() # self._num_random_points.setMinimum(2) # self._num_random_points.setMaximum(10000) # self._num_random_points.setValue(10) # self._num_random_points.setSuffix(" points") # bl.addWidget(self._num_random_points, 4, 1) btnDump = QPushButton("dump") btnDump.clicked.connect(self.dump_data) bl.addWidget(btnDump, 0, 2) self._cbType=cb=QComboBox() #cb.addItem("C") #cb.addItem("C++") #cb.addItems(["Fiducial", "FixTarget(12.5x12.5)", "FixTarget(23.0x23.0)", "FixTarget()", "Grid()"]) #cb.currentIndexChanged.connect(self.selectionchange) bl.addWidget(cb, 1,0,1,1) self._txtParam=param=QLineEdit() bl.addWidget(param, 1, 1,1,1) self._btnAdd=btnAdd = QPushButton("add obj") #btnAdd.clicked.connect(lambda x: _log.warning("TODO: IMPLEMENT") ) bl.addWidget(btnAdd, 1, 2,1,1) self._btnDelAll=btnDelAll = QPushButton("del all") #btnDelAll.clicked.connect(lambda x: _log.warning("TODO: IMPLEMENT") ) bl.addWidget(btnDelAll, 2, 2,1,1) self._btnFit=btnFit = QPushButton("fit with fiducials") #btnDelAll.clicked.connect(lambda x: _log.warning("TODO: IMPLEMENT") ) bl.addWidget(btnFit, 2, 1,1,1) layout.addWidget(frame) #self.markersTable.itemSelectionChanged.connect(self.selection_changed) #self.markersTable.setContextMenuPolicy(Qt.ActionsContextMenu) #deleteRowAction = QAction("Delete this row", self) #deleteRowAction.triggered.connect(self.delete_selected) #self.markersTable.addAction(deleteRowAction) #moveRequestAction = QAction("Move Here", self) #moveRequestAction.triggered.connect(self.request_stage_movement) #self.markersTable.addAction(moveRequestAction) #layout.addWidget(self.markersTable, stretch=2) #self.connect_all_signals() #self._yamlFn=fn=os.path.expanduser('~/.config/PSI/SwissMX.yaml') #self._tree=tree=pg.DataTreeWidget(data=self._data) self._data=data self._tree=tree=pg.DataTreeWidget(data=data) layout.addWidget(tree, stretch=2) #Context menu: by default the function # def contextMenuEvent(self, event) is called... but this requests to overload the function # and to build an own context menu # much simpler is to use Qt.ActionsContextMenu # and to handle the context in the parent plass tree.setContextMenuPolicy(Qt.ActionsContextMenu) act = QAction("delete", self) act.triggered.connect(self.tree_ctx_delete) tree.addAction(act) act = QAction("center in view", self) act.triggered.connect(self.tree_ctx_center) tree.addAction(act) #contextMenuEvent def tree_get_path(self): path=[] it=self._tree.currentItem() while it: d=it.data(0,0) try: d=int(d) except ValueError: pass if d!= '': path.append(d) it=it.parent() return path def tree_ctx_delete(self): app=QApplication.instance() path=self.tree_get_path() if len(path)==1: try: wnd=app._mainWnd except AttributeError: _log.info('_mainWnd not handeled') else: vb=wnd.vb grp=wnd._goTracked go=grp.childItems()[path[0]] vb.removeItem(go) data=grp.childItems() if not len(data): grp.setFlag(grp.ItemHasNoContents) self._tree.setData(data) def tree_ctx_center(self): app=QApplication.instance() path=self.tree_get_path() if len(path)==1: try: wnd=app._mainWnd except AttributeError: _log.info('_mainWnd not handeled') else: vb=wnd.vb grp=wnd._goTracked go=grp.childItems()[path[0]] vb.autoRange(items=(go,)) #r1=vb.viewRect() #r2=vb.itemBoundingRect(go) #if not r1.intersects(r2): def load_file(self, filename=None): app = QApplication.instance() if filename is None: filename, _ = QFileDialog.getOpenFileName(self,"Load data file",None, "json files (*.json);;all files (*)",) #"json files (*.json);;yaml files (*.yaml);;pickle files (*.pkl);;text files (*.txt);;all files (*)",) if not filename: # cancelled dialog return ext=filename.rsplit('.',1)[1].lower() if ext=='json': with open(filename, 'r') as f: data=json.load(f,object_hook=MyJsonDecoder) else: raise(IOError('unsupported file type')) self._tree.setData(data) try: wnd=app._mainWnd except AttributeError: _log.info('_mainWnd not handeled') pass else: grp=wnd._goTracked for go in data: grp.addItem(go) data=grp.childItems() self._tree.setData(data) #wnd._goTracked['objLst']=self._data return try: cfg.setValue("folders/last_prelocation_folder", QFileInfo(filename).canonicalPath()) cfg.setValue("folders/last_prelocation_sheet", QFileInfo(filename).absolutePath()) except TypeError as e: pass self.clearMarkers() _log.info(f"loading prelocated coords from {filename}") df = pd.read_csv(filename, comment="#", header=None, delim_whitespace=False, delimiter="[\t,;]") try: prefix = QFileInfo(filename).baseName() except: prefix = filename.name filename = prefix logger.info(f"prefix => {prefix}") gonio_coords_available = True for data in df.as_matrix(): # FIXME FutureWarning: Method .as_matrix will be removed in a future version. Use .values instead. row = self.markersTable.rowCount() self.markersTable.setRowCount(row + 1) original = np.copy(data) data = list(data) if len(data) in [2, 3]: gonio_coords_available = False data.extend([0, 0]) if len(data) == 4: data.insert(0, False) # fiducial flag elif len(data) == 5: # fiducial already there, convert to bool data[0] = bool(data[0]) else: QMessageBox.warning( self, "Wrong number of points in data file", "I was expecting either 2, 3, 4 or 5 data points per line." "\n\nFailed around a line with: {}".format(list(original)), ) self.addSingleMarker(row, data) self._xtals_transformed = False self.prefixSelected.emit(prefix) # only emit this signal if goniometer coordinates already read from file if gonio_coords_available: logger.debug(f"dataFileLoaded.emit => {filename}") self.dataFileLoaded.emit(filename) def save_file(self, filename=None): app = QApplication.instance() # filename = folders.get_file("prelocated-save.dat") #data_folder = settings.value("folders/last_prelocation_folder") data_folder='' if filename is None: filename, _ = QFileDialog.getSaveFileName(self,"Save data file",data_folder, "json files (*.json);;all files (*)",) #"json files (*.json);;yaml files (*.yaml);;pickle files (*.pkl);;text files (*.txt);;all files (*)",) if not filename: return #settings.setValue("folders/last_prelocation_folder", QFileInfo(filename).canonicalPath()) #_log.info("Saving data in {}".format(filename)) #data = self.get_data(as_numpy=True) #df = pd.DataFrame(data) #df.to_csv(filename, float_format="%.6f") #import numpy as np ext=filename.rsplit('.',1)[1].lower() try: wnd=app._mainWnd except AttributeError: _log.info('_mainWnd not handeled') data=self._data else: grp=wnd._goTracked data=grp.childItems() if ext=='json': with open(filename, 'w') as f: json.dump(data, f,cls=MyJsonEncoder, indent=2)#separators=(',', ':') else: raise(IOError('unsupported file type')) #elif ext=='yaml': # with open(filename, 'w') as f: # yaml.dump(self._data, f) #elif ext=='pkl': # with open(filename, 'wb') as f: # pickle.dump(self._data, f) #print(self._data) def dump_data(self): print(self._data) #for ref, x, y, cx, cy, ox, oy in self.get_data(): # print(f"fiducial:{ref} [{x}, {y}, {cx}, {cy}]") # **********3 OBSOLETE def delete_selected(self): row = self._current_row try: row += 0 except: _log.warning("select a row first") QMessageBox.warning(self,"Select a marker first","You must select a marker first before updating its goniometer position",) return self.markersTable.removeRow(row) self.prelocatedDataUpdated.emit() def selection_changed(self): row = self.markersTable.currentRow() if row < 0: return self._current_row = row self.selectedRowChanged.emit(row) _log.debug("selection changed: current row {}".format(row)) if __name__ == "__main__": import sys class Monster: yaml_tag = u'!Monster' def __init__(self, name, hp, ac, attacks): self.name = name self.hp = hp self.ac = ac self.attacks = attacks def __repr__(self): return "%s(name=%r, hp=%r, ac=%r, attacks=%r)" % ( self.__class__.__name__, self.name, self.hp, self.ac, self.attacks) class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.move(100,100) self.resize(500,800) self.centralWidget=QWidget() self.wndFixTrg=WndFixTarget(parent=self) self.setCentralWidget(self.centralWidget) mainLayout=QVBoxLayout() mainLayout.addWidget(self.wndFixTrg) self.centralWidget.setLayout(mainLayout) app = QApplication(sys.argv) mainWin = MainWindow() t=3 if t==0: data=[{'name':'John Doe', 'occupation':'gardener'}, {'name':'Lucy Black', 'occupation':'teacher'}] elif t==1: data ={ 'name': 'John Doe', 'occupation': 'gardener', 'A':(1,2,3), 'B':[1,2,3], 'C':{1,2,3}, 'D': {'1':(1,2,3), '2':'df', '3':'dddd'}, '4':np.array(((1,2,3),(4,5,6))), '5':np.array(range(40*2)).reshape(-1,2) } elif t==2: data = { 'a list': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], 'a dict': { 'x': 1, 'y': 2, 'z': 'three' }, 'an array': np.random.randint(10, size=(40,10)), #'a traceback': some_func1(), #'a function': some_func1, #'a class': pg.DataTreeWidget, } elif t==3: data=[ UsrGO.Grid(pos=(120.0, -100.0), size=(1000.0, 500.0), cnt=(10, 5), fiducialSize=2), Monster(name='Cave lizard', hp=[3,6], ac=16, attacks=['BITE','HURT']) ] mainWin.wndFixTrg._data=data mainWin.wndFixTrg._tree.setData(data) mainWin.show() sys.exit(app.exec_())