#!/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 logging _log=logging.getLogger(__name__) import os, json, base64, yaml import numpy as np import pyqtUsrObj as UsrGO import pyqtgraph as pg from PyQt5.QtCore import Qt, QFileInfo, pyqtSignal from PyQt5.QtWidgets import ( QFileDialog, QWidget, QGridLayout, QItemDelegate, QVBoxLayout, QPushButton, QApplication, QMainWindow, QLineEdit, QMessageBox, QAction, QComboBox) 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 iterencode(self, o, _one_shot=False): list_lvl = 0 l=super().iterencode(o, _one_shot=_one_shot) #l=tuple(l);print(''.join(l)) # helpful to debug for s in l: if s.startswith('['): list_lvl += 1 if list_lvl > 0: s = s[0]+s[1:].replace('\n', '').strip() s = s.replace('\n', '').rstrip() #self.item_separator): #self.key_separator if s.endswith(']'): list_lvl -= 1 yield s 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): 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) btnDump = QPushButton("dump") btnDump.clicked.connect(self.dump_data) bl.addWidget(btnDump, 0, 2) self._cbType=cb=QComboBox() 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._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 class tree.setContextMenuPolicy(Qt.ActionsContextMenu) act = QAction("center in view", self) act.triggered.connect(self.tree_ctx_center) tree.addAction(act) act = QAction("delete", self) act.triggered.connect(self.tree_ctx_delete) tree.addAction(act) act = QAction("update param", self) act.triggered.connect(self.tree_ctx_update) tree.addAction(act) def get_param(self): param=self._txtParam.text().replace('(','[').replace(')',']').strip() if param=='' or param[0]!='{': param='{'+param+'}' #mft._cbType.addItems(["Fiducial", "FixTarget(12.5x12.5)", "FixTarget(23.0x23.0)", "FixTarget()", "Grid()", "SwissMX-path"]) #bm_pos_eu=self._goBeamMarker._pos_eu #bm_size_eu=self._goBeamMarker._size_eu try: #parse the parameters: as yaml string. # allows : without space, allows () as [] # no {} to define a dictionary # e.g. 'a:ggf,b:5,c:[5,6.1],d(8,9,3)' param=param.replace(':', ': ') # allow gen:4 without space param=yaml.safe_load(param) # "ofs":[10, 5],"width":200,"fidScl":0.5,"fiducial":[[18,7],[25,16],[70, 20]] except BaseException as e: _log.error(f'{e}:{param}') param=dict() return param 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) #r=vb.viewRect() vb.setRange(rect=r2) r1.translate(r2.center()-r1.center()) vb.setRange(r1) def tree_ctx_update(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: grp=wnd._goTracked go=grp.childItems()[path[0]] go._param=self.get_param() data=grp.childItems() self._tree.setData(data) 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) if type(go)==UsrGO.Fiducial: go.sigRegionChangeFinished.connect(wnd.cb_fiducial_update_z) 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 base,ext=os.path.splitext(filename) if not ext.lower(): ext='json' filename=base+'.'+ext try: wnd=app._mainWnd except AttributeError: _log.info('_mainWnd not handeled') data=self._data else: grp=wnd._goTracked data=grp.childItems() if ext.lower()=='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_())