| Total Complexity | 112 | 
| Total Lines | 816 | 
| Duplicated Lines | 1.47 % | 
| Changes | 4 | ||
| Bugs | 3 | Features | 0 | 
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like PoiManagerLogic often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | # -*- coding: utf-8 -*-  | 
            ||
| 194 | class PoiManagerLogic(GenericLogic):  | 
            ||
| 195 | |||
| 196 | """  | 
            ||
| 197 | This is the Logic class for mapping and tracking bright features in the confocal scan.  | 
            ||
| 198 | """  | 
            ||
| 199 | _modclass = 'poimanagerlogic'  | 
            ||
| 200 | _modtype = 'logic'  | 
            ||
| 201 | |||
| 202 | # declare connectors  | 
            ||
| 203 | optimizer1 = Connector(interface='OptimizerLogic')  | 
            ||
| 204 | scannerlogic = Connector(interface='ConfocalLogic')  | 
            ||
| 205 | savelogic = Connector(interface='SaveLogic')  | 
            ||
| 206 | |||
| 207 | # status vars  | 
            ||
| 208 | poi_list = StatusVar(default=OrderedDict())  | 
            ||
| 209 | roi_name = StatusVar(default='')  | 
            ||
| 210 | active_poi = StatusVar(default=None)  | 
            ||
| 211 | |||
| 212 | signal_timer_updated = QtCore.Signal()  | 
            ||
| 213 | signal_poi_updated = QtCore.Signal()  | 
            ||
| 214 | signal_poi_deleted = QtCore.Signal(str)  | 
            ||
| 215 | signal_confocal_image_updated = QtCore.Signal()  | 
            ||
| 216 | signal_periodic_opt_started = QtCore.Signal()  | 
            ||
| 217 | signal_periodic_opt_duration_changed = QtCore.Signal()  | 
            ||
| 218 | signal_periodic_opt_stopped = QtCore.Signal()  | 
            ||
| 219 | |||
| 220 | def __init__(self, config, **kwargs):  | 
            ||
| 221 | super().__init__(config=config, **kwargs)  | 
            ||
| 222 | |||
| 223 | self._current_poi_key = None  | 
            ||
| 224 | self.go_to_crosshair_after_refocus = False # default value  | 
            ||
| 225 | |||
| 226 | # timer and its handling for the periodic refocus  | 
            ||
| 227 | self.timer = None  | 
            ||
| 228 | self.time_left = 0  | 
            ||
| 229 | self.timer_step = 0  | 
            ||
| 230 | self.timer_duration = 300  | 
            ||
| 231 | |||
| 232 | # locking for thread safety  | 
            ||
| 233 | self.threadlock = Mutex()  | 
            ||
| 234 | |||
| 235 | def on_activate(self):  | 
            ||
| 236 | """ Initialisation performed during activation of the module.  | 
            ||
| 237 | """  | 
            ||
| 238 | |||
| 239 | self._optimizer_logic = self.optimizer1()  | 
            ||
| 240 | self._confocal_logic = self.scannerlogic()  | 
            ||
| 241 | self._save_logic = self.savelogic()  | 
            ||
| 242 | |||
| 243 | # listen for the refocus to finish  | 
            ||
| 244 | self._optimizer_logic.sigRefocusFinished.connect(self._refocus_done)  | 
            ||
| 245 | |||
| 246 | # listen for the deactivation of a POI caused by moving to a different position  | 
            ||
| 247 | self._confocal_logic.signal_change_position.connect(self.user_move_deactivates_poi)  | 
            ||
| 248 | |||
| 249 | # Initialise the roi_map_data (xy confocal image)  | 
            ||
| 250 | self.roi_map_data = self._confocal_logic.xy_image  | 
            ||
| 251 | |||
| 252 | def on_deactivate(self):  | 
            ||
| 253 | return  | 
            ||
| 254 | |||
| 255 | def user_move_deactivates_poi(self, tag):  | 
            ||
| 256 | """ Deactivate the active POI if the confocal microscope scanner position is  | 
            ||
| 257 | moved by anything other than the optimizer  | 
            ||
| 258 | """  | 
            ||
| 259 | pass  | 
            ||
| 260 | |||
| 261 | def add_poi(self, position=None, key=None, emit_change=True):  | 
            ||
| 262 | """ Creates a new poi and adds it to the list.  | 
            ||
| 263 | |||
| 264 | @return int: key of this new poi  | 
            ||
| 265 | |||
| 266 | A position can be provided (such as during re-loading a saved ROI).  | 
            ||
| 267 | If no position is provided, then the current crosshair position is used.  | 
            ||
| 268 | """  | 
            ||
| 269 | # If there are only 2 POIs (sample and crosshair) then the newly added POI needs to start the sample drift logging.  | 
            ||
| 270 | if len(self.poi_list) == 2:  | 
            ||
| 271 | self.poi_list['sample']._creation_time = time.time()  | 
            ||
| 272 | # When the poimanager is activated the 'sample' poi is created because it is needed  | 
            ||
| 273 | # from the beginning for various functionalities. If the tracking of the sample is started it has  | 
            ||
| 274 | # to be reset such that this first point is deleted here  | 
            ||
| 275 | # Probably this can be solved a lot nicer.  | 
            ||
| 276 | self.poi_list['sample'].delete_last_position(empty_array_completely=True)  | 
            ||
| 277 | self.poi_list['sample'].add_position_to_history(position=[0, 0, 0])  | 
            ||
| 278 | self.poi_list['sample'].set_coords_in_sample(coords=[0, 0, 0])  | 
            ||
| 279 | |||
| 280 | if position is None:  | 
            ||
| 281 | position = self._confocal_logic.get_position()[:3]  | 
            ||
| 282 | if len(position) != 3:  | 
            ||
| 283 |             self.log.error('Given position is not 3-dimensional.' | 
            ||
| 284 | 'Please pass POIManager a 3-dimensional position to set a POI.')  | 
            ||
| 285 | return  | 
            ||
| 286 | |||
| 287 | new_poi = PoI(pos=position, key=key)  | 
            ||
| 288 | self.poi_list[new_poi.get_key()] = new_poi  | 
            ||
| 289 | |||
| 290 | # The POI coordinates are set relative to the last known sample position  | 
            ||
| 291 | most_recent_sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4]  | 
            ||
| 292 | this_poi_coords = position - most_recent_sample_pos  | 
            ||
| 293 | new_poi.set_coords_in_sample(coords=this_poi_coords)  | 
            ||
| 294 | |||
| 295 | # Since POI was created at current scanner position, it automatically  | 
            ||
| 296 | # becomes the active POI.  | 
            ||
| 297 | self.set_active_poi(poikey=new_poi.get_key())  | 
            ||
| 298 | |||
| 299 | if emit_change:  | 
            ||
| 300 | self.signal_poi_updated.emit()  | 
            ||
| 301 | |||
| 302 | return new_poi.get_key()  | 
            ||
| 303 | |||
| 304 | def get_confocal_image_data(self):  | 
            ||
| 305 | """ Get the current confocal xy scan data to hold as image of ROI"""  | 
            ||
| 306 | |||
| 307 | # get the roi_map_data (xy confocal image)  | 
            ||
| 308 | self.roi_map_data = self._confocal_logic.xy_image  | 
            ||
| 309 | |||
| 310 | self.signal_confocal_image_updated.emit()  | 
            ||
| 311 | |||
| 312 | def get_all_pois(self, abc_sort=False):  | 
            ||
| 313 | """ Returns a list of the names of all existing POIs.  | 
            ||
| 314 | |||
| 315 | @return string[]: List of names of the POIs  | 
            ||
| 316 | |||
| 317 | Also crosshair and sample are included.  | 
            ||
| 318 | """  | 
            ||
| 319 | if abc_sort is False:  | 
            ||
| 320 | return sorted(self.poi_list.keys())  | 
            ||
| 321 | |||
| 322 | elif abc_sort is True:  | 
            ||
| 323 | # First create a dictionary with poikeys indexed against names  | 
            ||
| 324 | poinames = [''] * len(self.poi_list.keys())  | 
            ||
| 325 | for i, poikey in enumerate(self.poi_list.keys()):  | 
            ||
| 326 | poiname = self.poi_list[poikey].get_name()  | 
            ||
| 327 | poinames[i] = [poiname, poikey]  | 
            ||
| 328 | |||
| 329 | # Sort names in the way that humans expect (site1, site2, site11, etc)  | 
            ||
| 330 | |||
| 331 | # Regular expressions to make sorting key  | 
            ||
| 332 | convert = lambda text: int(text) if text.isdigit() else text  | 
            ||
| 333 |             alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key[0])] | 
            ||
| 334 | # Now we can sort poinames by name and return keys in that order  | 
            ||
| 335 | return [key for [name, key] in sorted(poinames, key=alphanum_key)]  | 
            ||
| 336 | |||
| 337 | else:  | 
            ||
| 338 | # TODO: produce sensible error about unknown value of abc_sort.  | 
            ||
| 339 |             self.log.debug('fix TODO!') | 
            ||
| 340 | |||
| 341 | # TODO: Find a way to return a list of POI keys sorted in order of the POI names.  | 
            ||
| 342 | |||
| 343 | def delete_last_position(self, poikey=None):  | 
            ||
| 344 | """ Delete the last position in the history.  | 
            ||
| 345 | |||
| 346 | @param string poikey: the key of the poi  | 
            ||
| 347 | |||
| 348 | @return int: error code (0:OK, -1:error)  | 
            ||
| 349 | """  | 
            ||
| 350 | if poikey is not None and poikey in self.poi_list.keys():  | 
            ||
| 351 | self.poi_list[poikey].delete_last_position()  | 
            ||
| 352 | self.poi_list['sample'].delete_last_position()  | 
            ||
| 353 | self.signal_poi_updated.emit()  | 
            ||
| 354 | return 0  | 
            ||
| 355 | else:  | 
            ||
| 356 |             self.log.error('The last position of given POI ({0}) could not be deleted.'.format( | 
            ||
| 357 | poikey))  | 
            ||
| 358 | return -1  | 
            ||
| 359 | |||
| 360 | def delete_poi(self, poikey=None):  | 
            ||
| 361 | """ Completely deletes the whole given poi.  | 
            ||
| 362 | |||
| 363 | @param string poikey: the key of the poi  | 
            ||
| 364 | |||
| 365 | @return int: error code (0:OK, -1:error)  | 
            ||
| 366 | |||
| 367 | Does not delete the crosshair and sample.  | 
            ||
| 368 | """  | 
            ||
| 369 | |||
| 370 | if poikey is not None and poikey in self.poi_list.keys():  | 
            ||
| 371 | if poikey is 'crosshair' or poikey is 'sample':  | 
            ||
| 372 |                 self.log.warning('You cannot delete the crosshair or sample.') | 
            ||
| 373 | return -1  | 
            ||
| 374 | del self.poi_list[poikey]  | 
            ||
| 375 | |||
| 376 | # If the active poi was deleted, there is no way to automatically choose  | 
            ||
| 377 | # another active POI, so we deactivate POI  | 
            ||
| 378 | if self.active_poi is not None and poikey == self.active_poi.get_key():  | 
            ||
| 379 | self._deactivate_poi()  | 
            ||
| 380 | |||
| 381 | self.signal_poi_updated.emit()  | 
            ||
| 382 | self.signal_poi_deleted.emit(poikey)  | 
            ||
| 383 | return 0  | 
            ||
| 384 | elif poikey is None:  | 
            ||
| 385 |             self.log.warning('No POI for deletion specified.') | 
            ||
| 386 | else:  | 
            ||
| 387 |             self.log.error('X. The given POI ({0}) does not exist.'.format( | 
            ||
| 388 | poikey))  | 
            ||
| 389 | return -1  | 
            ||
| 390 | |||
| 391 | def optimise_poi(self, poikey=None):  | 
            ||
| 392 | """ Starts the optimisation procedure for the given poi.  | 
            ||
| 393 | |||
| 394 | @param string poikey: the key of the poi  | 
            ||
| 395 | |||
| 396 | @return int: error code (0:OK, -1:error)  | 
            ||
| 397 | |||
| 398 | This is threaded, so it returns directly.  | 
            ||
| 399 | The function _refocus_done handles the data when the optimisation returns.  | 
            ||
| 400 | """  | 
            ||
| 401 | |||
| 402 | if poikey is not None and poikey in self.poi_list.keys():  | 
            ||
| 403 | self.poi_list['crosshair'].add_position_to_history(position=self._confocal_logic.get_position()[:3])  | 
            ||
| 404 | self._current_poi_key = poikey  | 
            ||
| 405 | self._optimizer_logic.start_refocus(  | 
            ||
| 406 | initial_pos=self.get_poi_position(poikey=poikey),  | 
            ||
| 407 | caller_tag='poimanager')  | 
            ||
| 408 | return 0  | 
            ||
| 409 | else:  | 
            ||
| 410 | self.log.error(  | 
            ||
| 411 |                 'Z. The given POI ({0}) does not exist.'.format(poikey)) | 
            ||
| 412 | return -1  | 
            ||
| 413 | |||
| 414 | def go_to_poi(self, poikey=None):  | 
            ||
| 415 | """ Goes to the given poi and saves it as the current one.  | 
            ||
| 416 | |||
| 417 | @param string poikey: the key of the poi  | 
            ||
| 418 | |||
| 419 | @return int: error code (0:OK, -1:error)  | 
            ||
| 420 | """  | 
            ||
| 421 | if poikey is not None and poikey in self.poi_list.keys():  | 
            ||
| 422 | self._current_poi_key = poikey  | 
            ||
| 423 | x, y, z = self.get_poi_position(poikey=poikey)  | 
            ||
| 424 |             self._confocal_logic.set_position('poimanager', x=x, y=y, z=z) | 
            ||
| 425 | else:  | 
            ||
| 426 |             self.log.error('The given POI ({0}) does not exist.'.format( | 
            ||
| 427 | poikey))  | 
            ||
| 428 | return -1  | 
            ||
| 429 | # This is now the active POI to send to save logic for naming in any saved filenames.  | 
            ||
| 430 | self.set_active_poi(poikey)  | 
            ||
| 431 | |||
| 432 | #Fixme: After pressing the Go to Poi button the active poi is empty and the following lines do fix this  | 
            ||
| 433 | # The time.sleep is somehow needed if not active_poi can not be set  | 
            ||
| 434 | time.sleep(0.001)  | 
            ||
| 435 | self.active_poi = self.poi_list[poikey]  | 
            ||
| 436 | self.signal_poi_updated.emit()  | 
            ||
| 437 | |||
| 438 | def get_poi_position(self, poikey=None):  | 
            ||
| 439 | """ Returns the current position of the given poi, calculated from the  | 
            ||
| 440 | POI coords in sample and the current sample position.  | 
            ||
| 441 | |||
| 442 | @param string poikey: the key of the poi  | 
            ||
| 443 | |||
| 444 | @return  | 
            ||
| 445 | """  | 
            ||
| 446 | |||
| 447 | if poikey is not None and poikey in self.poi_list.keys():  | 
            ||
| 448 | |||
| 449 | poi_coords = self.poi_list[poikey].get_coords_in_sample()  | 
            ||
| 450 | sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4]  | 
            ||
| 451 | return sample_pos + poi_coords  | 
            ||
| 452 | |||
| 453 | else:  | 
            ||
| 454 |             self.log.error('G. The given POI ({0}) does not exist.'.format( | 
            ||
| 455 | poikey))  | 
            ||
| 456 | return [-1., -1., -1.]  | 
            ||
| 457 | |||
| 458 | def set_new_position(self, poikey=None, newpos=None):  | 
            ||
| 459 | """  | 
            ||
| 460 | Moves the given POI to a new position, and uses this information to update  | 
            ||
| 461 | the sample position.  | 
            ||
| 462 | |||
| 463 | @param string poikey: the key of the poi  | 
            ||
| 464 | @param float[3] newpos: coordinates of the new position  | 
            ||
| 465 | |||
| 466 | @return int: error code (0:OK, -1:error)  | 
            ||
| 467 | """  | 
            ||
| 468 | |||
| 469 | # If no new position is given, take the current confocal crosshair position  | 
            ||
| 470 | if newpos is None:  | 
            ||
| 471 | newpos = self._confocal_logic.get_position()[:3]  | 
            ||
| 472 | |||
| 473 | if poikey is not None and poikey in self.poi_list.keys():  | 
            ||
| 474 | if len(newpos) != 3:  | 
            ||
| 475 |                 self.log.error('Length of set poi is not 3.') | 
            ||
| 476 | return -1  | 
            ||
| 477 | # Add new position to trace of POI  | 
            ||
| 478 | self.poi_list[poikey].add_position_to_history(position=newpos)  | 
            ||
| 479 | |||
| 480 | # Calculate sample shift and add it to the trace of 'sample' POI  | 
            ||
| 481 | sample_shift = newpos - self.get_poi_position(poikey=poikey)  | 
            ||
| 482 | sample_shift += self.poi_list['sample'].get_position_history()[-1, :][1:4]  | 
            ||
| 483 | self.poi_list['sample'].add_position_to_history(position=sample_shift)  | 
            ||
| 484 | |||
| 485 | # signal POI has been updated (this will cause GUI to redraw)  | 
            ||
| 486 | if (poikey is not 'crosshair') and (poikey is not 'sample'):  | 
            ||
| 487 | self.signal_poi_updated.emit()  | 
            ||
| 488 | |||
| 489 | return 0  | 
            ||
| 490 | |||
| 491 |         self.log.error('J. The given POI ({0}) does not exist.'.format(poikey)) | 
            ||
| 492 | return -1  | 
            ||
| 493 | |||
| 494 | def move_coords(self, poikey=None, newpos=None):  | 
            ||
| 495 | """Updates the coords of a given POI, and adds a position to the POI history,  | 
            ||
| 496 | but DOES NOT update the sample position.  | 
            ||
| 497 | """  | 
            ||
| 498 | if newpos is None:  | 
            ||
| 499 | newpos = self._confocal_logic.get_position()[:3]  | 
            ||
| 500 | |||
| 501 | if poikey is not None and poikey in self.poi_list.keys():  | 
            ||
| 502 | if len(newpos) != 3:  | 
            ||
| 503 |                 self.log.error('Length of set poi is not 3.') | 
            ||
| 504 | return -1  | 
            ||
| 505 | this_poi = self.poi_list[poikey]  | 
            ||
| 506 | return_val = this_poi.add_position_to_history(position=newpos)  | 
            ||
| 507 | |||
| 508 | sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4]  | 
            ||
| 509 | |||
| 510 | new_coords = newpos - sample_pos  | 
            ||
| 511 | |||
| 512 | this_poi.set_coords_in_sample(new_coords)  | 
            ||
| 513 | |||
| 514 | self.signal_poi_updated.emit()  | 
            ||
| 515 | |||
| 516 | return return_val  | 
            ||
| 517 | |||
| 518 |         self.log.error('JJ. The given POI ({0}) does not exist.'.format(poikey)) | 
            ||
| 519 | return -1  | 
            ||
| 520 | |||
| 521 | def rename_poi(self, poikey=None, name=None, emit_change=True):  | 
            ||
| 522 | """ Sets the name of the given poi.  | 
            ||
| 523 | |||
| 524 | @param string poikey: the key of the poi  | 
            ||
| 525 | @param string name: name of the poi to be set  | 
            ||
| 526 | |||
| 527 | @return int: error code (0:OK, -1:error)  | 
            ||
| 528 | """  | 
            ||
| 529 | |||
| 530 | if poikey is not None and name is not None and poikey in self.poi_list.keys():  | 
            ||
| 531 | |||
| 532 | success = self.poi_list[poikey].set_name(name=name)  | 
            ||
| 533 | |||
| 534 | # if this is the active POI then we need to update poi tag in savelogic  | 
            ||
| 535 | if self.poi_list[poikey] == self.active_poi:  | 
            ||
| 536 | self.update_poi_tag_in_savelogic()  | 
            ||
| 537 | |||
| 538 | if emit_change:  | 
            ||
| 539 | self.signal_poi_updated.emit()  | 
            ||
| 540 | |||
| 541 | return success  | 
            ||
| 542 | |||
| 543 | else:  | 
            ||
| 544 |             self.log.error('AAAThe given POI ({0}) does not exist.'.format( | 
            ||
| 545 | poikey))  | 
            ||
| 546 | return -1  | 
            ||
| 547 | |||
| 548 | def start_periodic_refocus(self, poikey=None):  | 
            ||
| 549 | """ Starts the perodic refocussing of the poi.  | 
            ||
| 550 | |||
| 551 | @param float duration: (optional) the time between periodic optimization  | 
            ||
| 552 | @param string poikey: (optional) the key of the poi to be set and refocussed on.  | 
            ||
| 553 | |||
| 554 | @return int: error code (0:OK, -1:error)  | 
            ||
| 555 | """  | 
            ||
| 556 | |||
| 557 | if poikey is not None and poikey in self.poi_list.keys():  | 
            ||
| 558 | self._current_poi_key = poikey  | 
            ||
| 559 | else:  | 
            ||
| 560 | # Todo: warning message that active POI used by default  | 
            ||
| 561 | self._current_poi_key = self.active_poi.get_key()  | 
            ||
| 562 | |||
| 563 |         self.log.info('Periodic refocus on {0}.'.format(self._current_poi_key)) | 
            ||
| 564 | |||
| 565 | self.timer_step = 0  | 
            ||
| 566 | self.timer = QtCore.QTimer()  | 
            ||
| 567 | self.timer.setSingleShot(False)  | 
            ||
| 568 | self.timer.timeout.connect(self._periodic_refocus_loop)  | 
            ||
| 569 | self.timer.start(300)  | 
            ||
| 570 | |||
| 571 | self.signal_periodic_opt_started.emit()  | 
            ||
| 572 | return 0  | 
            ||
| 573 | |||
| 574 | def set_periodic_optimize_duration(self, duration=None):  | 
            ||
| 575 | """ Change the duration of the periodic optimize timer during active  | 
            ||
| 576 | periodic refocussing.  | 
            ||
| 577 | |||
| 578 | @param float duration: (optional) the time between periodic optimization.  | 
            ||
| 579 | """  | 
            ||
| 580 | if duration is not None:  | 
            ||
| 581 | self.timer_duration = duration  | 
            ||
| 582 | else:  | 
            ||
| 583 |             self.log.warning('No timer duration given, using {0} s.'.format( | 
            ||
| 584 | self.timer_duration))  | 
            ||
| 585 | |||
| 586 | self.signal_periodic_opt_duration_changed.emit()  | 
            ||
| 587 | |||
| 588 | def _periodic_refocus_loop(self):  | 
            ||
| 589 | """ This is the looped function that does the actual periodic refocus.  | 
            ||
| 590 | |||
| 591 | If the time has run out, it refocussed the current poi.  | 
            ||
| 592 | Otherwise it just updates the time that is left.  | 
            ||
| 593 | """  | 
            ||
| 594 | self.time_left = self.timer_step - time.time() + self.timer_duration  | 
            ||
| 595 | self.signal_timer_updated.emit()  | 
            ||
| 596 | if self.time_left <= 0:  | 
            ||
| 597 | self.timer_step = time.time()  | 
            ||
| 598 | self.optimise_poi(poikey=self._current_poi_key)  | 
            ||
| 599 | |||
| 600 | def stop_periodic_refocus(self):  | 
            ||
| 601 | """ Stops the perodic refocussing of the poi.  | 
            ||
| 602 | |||
| 603 | @return int: error code (0:OK, -1:error)  | 
            ||
| 604 | """  | 
            ||
| 605 | if self.timer is None:  | 
            ||
| 606 |             self.log.warning('No timer to stop.') | 
            ||
| 607 | return -1  | 
            ||
| 608 | self.timer.stop()  | 
            ||
| 609 | self.timer = None  | 
            ||
| 610 | |||
| 611 | self.signal_periodic_opt_stopped.emit()  | 
            ||
| 612 | return 0  | 
            ||
| 613 | |||
| 614 | def _refocus_done(self, caller_tag, optimal_pos):  | 
            ||
| 615 | """ Gets called automatically after the refocus is done and saves the new position  | 
            ||
| 616 | to the poi history.  | 
            ||
| 617 | |||
| 618 | Also it tracks the sample and may go back to the crosshair.  | 
            ||
| 619 | |||
| 620 | @return int: error code (0:OK, -1:error)  | 
            ||
| 621 | """  | 
            ||
| 622 | # We only need x, y, z  | 
            ||
| 623 | optimized_position = optimal_pos[0:3]  | 
            ||
| 624 | |||
| 625 | # If the refocus was on the crosshair, then only update crosshair POI and don't  | 
            ||
| 626 | # do anything with sample position.  | 
            ||
| 627 | caller_tags = ['confocalgui', 'magnet_logic', 'singleshot_logic']  | 
            ||
| 628 | if caller_tag in caller_tags:  | 
            ||
| 629 | self.poi_list['crosshair'].add_position_to_history(position=optimized_position)  | 
            ||
| 630 | |||
| 631 | # If the refocus was initiated here by poimanager, then update POI and sample  | 
            ||
| 632 | elif caller_tag == 'poimanager':  | 
            ||
| 633 | |||
| 634 | if self._current_poi_key is not None and self._current_poi_key in self.poi_list.keys():  | 
            ||
| 635 | |||
| 636 | self.set_new_position(poikey=self._current_poi_key, newpos=optimized_position)  | 
            ||
| 637 | |||
| 638 | if self.go_to_crosshair_after_refocus:  | 
            ||
| 639 | temp_key = self._current_poi_key  | 
            ||
| 640 | self.go_to_poi(poikey='crosshair')  | 
            ||
| 641 | self._current_poi_key = temp_key  | 
            ||
| 642 | else:  | 
            ||
| 643 | self.go_to_poi(poikey=self._current_poi_key)  | 
            ||
| 644 | return 0  | 
            ||
| 645 | else:  | 
            ||
| 646 |                 self.log.error('The given POI ({0}) does not exist.'.format( | 
            ||
| 647 | self._current_poi_key))  | 
            ||
| 648 | return -1  | 
            ||
| 649 | |||
| 650 | else:  | 
            ||
| 651 |             self.log.warning("Unknown caller_tag for the optimizer. POI " | 
            ||
| 652 | "Manager does not know what to do with optimized "  | 
            ||
| 653 | "position, and has done nothing.")  | 
            ||
| 654 | |||
| 655 | def reset_roi(self):  | 
            ||
| 656 | |||
| 657 | del self.poi_list  | 
            ||
| 658 | self.poi_list = dict()  | 
            ||
| 659 | |||
| 660 | self.active_poi = None  | 
            ||
| 661 | |||
| 662 | self.roi_name = ''  | 
            ||
| 663 | |||
| 664 | # initally add crosshair to the pois  | 
            ||
| 665 | crosshair = PoI(pos=[0, 0, 0], name='crosshair')  | 
            ||
| 666 | crosshair._key = 'crosshair'  | 
            ||
| 667 | self.poi_list[crosshair._key] = crosshair  | 
            ||
| 668 | |||
| 669 | # Re-initialise sample in the poi list  | 
            ||
| 670 | sample = PoI(pos=[0, 0, 0], name='sample')  | 
            ||
| 671 | sample._key = 'sample'  | 
            ||
| 672 | self.poi_list[sample._key] = sample  | 
            ||
| 673 | |||
| 674 | self.signal_poi_updated.emit()  | 
            ||
| 675 | |||
| 676 | def set_active_poi(self, poikey=None):  | 
            ||
| 677 | """  | 
            ||
| 678 | Set the active POI object.  | 
            ||
| 679 | """  | 
            ||
| 680 | |||
| 681 | if poikey is None:  | 
            ||
| 682 | # If poikey is none and no active poi is set, then do nothing  | 
            ||
| 683 | if self.active_poi is None:  | 
            ||
| 684 | return  | 
            ||
| 685 | else:  | 
            ||
| 686 | self.active_poi = None  | 
            ||
| 687 | |||
| 688 | elif poikey in self.get_all_pois():  | 
            ||
| 689 | # If poikey is the current active POI then do nothing  | 
            ||
| 690 | if self.poi_list[poikey] == self.active_poi:  | 
            ||
| 691 | return  | 
            ||
| 692 | |||
| 693 | else:  | 
            ||
| 694 | self.active_poi = self.poi_list[poikey]  | 
            ||
| 695 | |||
| 696 | else:  | 
            ||
| 697 | # todo: error poikey unknown  | 
            ||
| 698 | return -1  | 
            ||
| 699 | |||
| 700 | self.update_poi_tag_in_savelogic()  | 
            ||
| 701 | self.signal_poi_updated.emit() # todo: this breaks the emit_change = false case  | 
            ||
| 702 | |||
| 703 | def _deactivate_poi(self):  | 
            ||
| 704 | self.set_active_poi(poikey=None)  | 
            ||
| 705 | |||
| 706 | def update_poi_tag_in_savelogic(self):  | 
            ||
| 707 | |||
| 708 | if self.active_poi is not None:  | 
            ||
| 709 | self._save_logic.active_poi_name = self.active_poi.get_name()  | 
            ||
| 710 | else:  | 
            ||
| 711 | self._save_logic.active_poi_name = ''  | 
            ||
| 712 | |||
| 713 | def save_poi_map_as_roi(self):  | 
            ||
| 714 | """ Save a list of POIs with their coordinates to a file.  | 
            ||
| 715 | """  | 
            ||
| 716 | # File path and name  | 
            ||
| 717 | filepath = self._save_logic.get_path_for_module(module_name='ROIs')  | 
            ||
| 718 | |||
| 719 | # We will fill the data OderedDict to send to savelogic  | 
            ||
| 720 | data = OrderedDict()  | 
            ||
| 721 | |||
| 722 | # Lists for each column of the output file  | 
            ||
| 723 | poinames = []  | 
            ||
| 724 | poikeys = []  | 
            ||
| 725 | x_coords = []  | 
            ||
| 726 | y_coords = []  | 
            ||
| 727 | z_coords = []  | 
            ||
| 728 | |||
| 729 | for poikey in self.get_all_pois(abc_sort=True):  | 
            ||
| 730 | if poikey is not 'sample' and poikey is not 'crosshair':  | 
            ||
| 731 | thispoi = self.poi_list[poikey]  | 
            ||
| 732 | |||
| 733 | poinames.append(thispoi.get_name())  | 
            ||
| 734 | poikeys.append(poikey)  | 
            ||
| 735 | x_coords.append(thispoi.get_coords_in_sample()[0])  | 
            ||
| 736 | y_coords.append(thispoi.get_coords_in_sample()[1])  | 
            ||
| 737 | z_coords.append(thispoi.get_coords_in_sample()[2])  | 
            ||
| 738 | |||
| 739 | data['POI Name'] = np.array(poinames)  | 
            ||
| 740 | data['POI Key'] = np.array(poikeys)  | 
            ||
| 741 | data['X'] = np.array(x_coords)  | 
            ||
| 742 | data['Y'] = np.array(y_coords)  | 
            ||
| 743 | data['Z'] = np.array(z_coords)  | 
            ||
| 744 | |||
| 745 | self._save_logic.save_data(  | 
            ||
| 746 | data,  | 
            ||
| 747 | filepath=filepath,  | 
            ||
| 748 | filelabel=self.roi_name,  | 
            ||
| 749 | fmt=['%s', '%s', '%.6e', '%.6e', '%.6e']  | 
            ||
| 750 | )  | 
            ||
| 751 | |||
| 752 |         self.log.debug('ROI saved to:\n{0}'.format(filepath)) | 
            ||
| 753 | return 0  | 
            ||
| 754 | |||
| 755 | def load_roi_from_file(self, filename=None):  | 
            ||
| 756 | |||
| 757 | if filename is None:  | 
            ||
| 758 | return -1  | 
            ||
| 759 | |||
| 760 | with open(filename, 'r') as roifile:  | 
            ||
| 761 | for line in roifile:  | 
            ||
| 762 | if line[0] != '#' and line.split()[0] != 'NaN':  | 
            ||
| 763 | saved_poi_name = line.split()[0]  | 
            ||
| 764 | saved_poi_key = line.split()[1]  | 
            ||
| 765 | saved_poi_coords = [  | 
            ||
| 766 | float(line.split()[2]), float(line.split()[3]), float(line.split()[4])]  | 
            ||
| 767 | |||
| 768 | this_poi_key = self.add_poi(  | 
            ||
| 769 | position=saved_poi_coords,  | 
            ||
| 770 | key=saved_poi_key,  | 
            ||
| 771 | emit_change=False)  | 
            ||
| 772 | self.rename_poi(poikey=this_poi_key, name=saved_poi_name, emit_change=False)  | 
            ||
| 773 | |||
| 774 | # Now that all the POIs are created, emit the signal for other things (ie gui) to update  | 
            ||
| 775 | self.signal_poi_updated.emit()  | 
            ||
| 776 | return 0  | 
            ||
| 777 | |||
| 778 | @poi_list.constructor  | 
            ||
| 779 | def dict_to_poi_list(self, val):  | 
            ||
| 780 |         pdict = {} | 
            ||
| 781 | # initially add crosshair to the pois  | 
            ||
| 782 | crosshair = PoI(pos=[0, 0, 0], name='crosshair')  | 
            ||
| 783 | crosshair._key = 'crosshair'  | 
            ||
| 784 | pdict[crosshair._key] = crosshair  | 
            ||
| 785 | |||
| 786 | # initally add sample to the pois  | 
            ||
| 787 | sample = PoI(pos=[0, 0, 0], name='sample')  | 
            ||
| 788 | sample._key = 'sample'  | 
            ||
| 789 | pdict[sample._key] = sample  | 
            ||
| 790 | |||
| 791 | if isinstance(val, dict):  | 
            ||
| 792 | for key, poidict in val.items():  | 
            ||
| 793 | try:  | 
            ||
| 794 | View Code Duplication | if len(poidict['pos']) >= 3:  | 
            |
| 
                                                                                                    
                        
                         | 
                |||
| 795 | newpoi = PoI(name=poidict['name'], key=poidict['key'])  | 
            ||
| 796 | newpoi.set_coords_in_sample(poidict['pos'])  | 
            ||
| 797 | newpoi._creation_time = poidict['time']  | 
            ||
| 798 | newpoi._position_time_trace = poidict['history']  | 
            ||
| 799 | pdict[key] = newpoi  | 
            ||
| 800 | except Exception as e:  | 
            ||
| 801 |                     self.log.exception('Could not load PoI {0}: {1}'.format(key, poidict)) | 
            ||
| 802 | return pdict  | 
            ||
| 803 | |||
| 804 | @poi_list.representer  | 
            ||
| 805 | def poi_list_to_dict(self, val):  | 
            ||
| 806 |         pdict = { | 
            ||
| 807 | key: poi.to_dict() for key, poi in val.items()  | 
            ||
| 808 | }  | 
            ||
| 809 | return pdict  | 
            ||
| 810 | |||
| 811 | @active_poi.representer  | 
            ||
| 812 | def active_poi_to_dict(self, val):  | 
            ||
| 813 | if isinstance(val, PoI):  | 
            ||
| 814 | return val.to_dict()  | 
            ||
| 815 | return None  | 
            ||
| 816 | |||
| 817 | @active_poi.constructor  | 
            ||
| 818 | def dict_to_active_poi(self, val):  | 
            ||
| 819 | try:  | 
            ||
| 820 | View Code Duplication | if isinstance(val, dict):  | 
            |
| 821 | if len(val['pos']) >= 3:  | 
            ||
| 822 | newpoi = PoI(pos=val['pos'], name=val['name'], key=val['key'])  | 
            ||
| 823 | newpoi._creation_time = val['time']  | 
            ||
| 824 | newpoi._position_time_trace = val['history']  | 
            ||
| 825 | return newpoi  | 
            ||
| 826 | except Exception as e:  | 
            ||
| 827 |             self.log.exception('Could not load active poi {0}'.format(val)) | 
            ||
| 828 | return None  | 
            ||
| 829 | |||
| 830 | def triangulate(self, r, a1, b1, c1, a2, b2, c2):  | 
            ||
| 831 | """ Reorients a coordinate r that is known relative to reference points a1, b1, c1 to  | 
            ||
| 832 | produce a new vector rnew that has exactly the same relation to rotated/shifted/tilted  | 
            ||
| 833 | reference positions a2, b2, c2.  | 
            ||
| 834 | |||
| 835 | @param np.array r: position to be remapped.  | 
            ||
| 836 | |||
| 837 | @param np.array a1: initial location of ref1.  | 
            ||
| 838 | |||
| 839 | @param np.array a2: final location of ref1.  | 
            ||
| 840 | |||
| 841 | @param np.array b1, b2, c1, c2: similar for ref2 and ref3  | 
            ||
| 842 | """  | 
            ||
| 843 | |||
| 844 | ab_old = b1 - a1  | 
            ||
| 845 | ac_old = c1 - a1  | 
            ||
| 846 | |||
| 847 | ab_new = b2 - a2  | 
            ||
| 848 | ac_new = c2 - a2  | 
            ||
| 849 | |||
| 850 | # Firstly, find the angle to rotate ab_old onto ab_new. This rotation must be done in  | 
            ||
| 851 | # the plane that contains these two vectors, which means rotating about an axis  | 
            ||
| 852 | # perpendicular to both of them (the cross product).  | 
            ||
| 853 | |||
| 854 | axis1 = np.cross(ab_old, ab_new) # Only works if ab_old and ab_new are not parallel  | 
            ||
| 855 | axis1length = np.sqrt((axis1 * axis1).sum())  | 
            ||
| 856 | |||
| 857 | if axis1length == 0:  | 
            ||
| 858 | ab_olddif = ab_old + np.array([100, 0, 0])  | 
            ||
| 859 | axis1 = np.cross(ab_old, ab_olddif)  | 
            ||
| 860 | |||
| 861 | # normalising the axis1 vector  | 
            ||
| 862 | axis1 = axis1 / np.sqrt((axis1 * axis1).sum())  | 
            ||
| 863 | |||
| 864 | # The dot product gives the angle between ab_old and ab_new  | 
            ||
| 865 | dot = np.dot(ab_old, ab_new)  | 
            ||
| 866 | x_modulus = np.sqrt((ab_old * ab_old).sum())  | 
            ||
| 867 | y_modulus = np.sqrt((ab_new * ab_new).sum())  | 
            ||
| 868 | |||
| 869 | # float errors can cause the division to be slightly above 1 for 90 degree rotations, which  | 
            ||
| 870 | # will confuse arccos.  | 
            ||
| 871 | cos_angle = min(dot / x_modulus / y_modulus, 1)  | 
            ||
| 872 | |||
| 873 | angle1 = np.arccos(cos_angle) # angle in radians  | 
            ||
| 874 | |||
| 875 | # Construct a rotational matrix for axis1  | 
            ||
| 876 | n1 = axis1[0]  | 
            ||
| 877 | n2 = axis1[1]  | 
            ||
| 878 | n3 = axis1[2]  | 
            ||
| 879 | |||
| 880 | m1 = np.matrix(((((n1 * n1) * (1 - np.cos(angle1)) + np.cos(angle1)),  | 
            ||
| 881 | ((n1 * n2) * (1 - np.cos(angle1)) - n3 * np.sin(angle1)),  | 
            ||
| 882 | ((n1 * n3) * (1 - np.cos(angle1)) + n2 * np.sin(angle1))  | 
            ||
| 883 | ),  | 
            ||
| 884 | (((n2 * n1) * (1 - np.cos(angle1)) + n3 * np.sin(angle1)),  | 
            ||
| 885 | ((n2 * n2) * (1 - np.cos(angle1)) + np.cos(angle1)),  | 
            ||
| 886 | ((n2 * n3) * (1 - np.cos(angle1)) - n1 * np.sin(angle1))  | 
            ||
| 887 | ),  | 
            ||
| 888 | (((n3 * n1) * (1 - np.cos(angle1)) - n2 * np.sin(angle1)),  | 
            ||
| 889 | ((n3 * n2) * (1 - np.cos(angle1)) + n1 * np.sin(angle1)),  | 
            ||
| 890 | ((n3 * n3) * (1 - np.cos(angle1)) + np.cos(angle1))  | 
            ||
| 891 | )  | 
            ||
| 892 | )  | 
            ||
| 893 | )  | 
            ||
| 894 | |||
| 895 | # Now that ab_old can be rotated to overlap with ab_new, we need to rotate in another  | 
            ||
| 896 | # axis to fix "tilt". By choosing ab_new as the rotation axis we ensure that the  | 
            ||
| 897 | # ab vectors stay where they need to be.  | 
            ||
| 898 | |||
| 899 | # ac_old_rot is the rotated ac_old (around axis1). We need to find the angle to rotate  | 
            ||
| 900 | # ac_old_rot around ab_new to get ac_new.  | 
            ||
| 901 | ac_old_rot = np.array(np.dot(m1, ac_old))[0]  | 
            ||
| 902 | |||
| 903 | axis2 = -ab_new # TODO: check maths to find why this negative sign is necessary. Empirically it is now working.  | 
            ||
| 904 | axis2 = axis2 / np.sqrt((axis2 * axis2).sum())  | 
            ||
| 905 | |||
| 906 | # To get the angle of rotation it is most convenient to work in the plane for which axis2 is the normal.  | 
            ||
| 907 | # We must project vectors ac_old_rot and ac_new into this plane.  | 
            ||
| 908 | a = ac_old_rot - np.dot(ac_old_rot, axis2) * axis2 # projection of ac_old_rot in the plane of rotation about axis2  | 
            ||
| 909 | b = ac_new - np.dot(ac_new, axis2) * axis2 # projection of ac_new in the plane of rotation about axis2  | 
            ||
| 910 | |||
| 911 | # The dot product gives the angle of rotation around axis2  | 
            ||
| 912 | dot = np.dot(a, b)  | 
            ||
| 913 | |||
| 914 | x_modulus = np.sqrt((a * a).sum())  | 
            ||
| 915 | y_modulus = np.sqrt((b * b).sum())  | 
            ||
| 916 | cos_angle = min(dot / x_modulus / y_modulus, 1) # float errors can cause the division to be slightly above 1 for 90 degree rotations, which will confuse arccos.  | 
            ||
| 917 | angle2 = np.arccos(cos_angle) # angle in radians  | 
            ||
| 918 | |||
| 919 | # Construct a rotation matrix around axis2  | 
            ||
| 920 | n1 = axis2[0]  | 
            ||
| 921 | n2 = axis2[1]  | 
            ||
| 922 | n3 = axis2[2]  | 
            ||
| 923 | |||
| 924 | m2 = np.matrix(((((n1 * n1) * (1 - np.cos(angle2)) + np.cos(angle2)),  | 
            ||
| 925 | ((n1 * n2) * (1 - np.cos(angle2)) - n3 * np.sin(angle2)),  | 
            ||
| 926 | ((n1 * n3) * (1 - np.cos(angle2)) + n2 * np.sin(angle2))  | 
            ||
| 927 | ),  | 
            ||
| 928 | (((n2 * n1) * (1 - np.cos(angle2)) + n3 * np.sin(angle2)),  | 
            ||
| 929 | ((n2 * n2) * (1 - np.cos(angle2)) + np.cos(angle2)),  | 
            ||
| 930 | ((n2 * n3) * (1 - np.cos(angle2)) - n1 * np.sin(angle2))  | 
            ||
| 931 | ),  | 
            ||
| 932 | (((n3 * n1) * (1 - np.cos(angle2)) - n2 * np.sin(angle2)),  | 
            ||
| 933 | ((n3 * n2) * (1 - np.cos(angle2)) + n1 * np.sin(angle2)),  | 
            ||
| 934 | ((n3 * n3) * (1 - np.cos(angle2)) + np.cos(angle2))  | 
            ||
| 935 | )  | 
            ||
| 936 | )  | 
            ||
| 937 | )  | 
            ||
| 938 | |||
| 939 | # To find the new position of r, displace by (a2 - a1) and do the rotations  | 
            ||
| 940 | a1r = r - a1  | 
            ||
| 941 | |||
| 942 | rnew = a2 + np.array(np.dot(m2, np.array(np.dot(m1, a1r))[0]))[0]  | 
            ||
| 943 | |||
| 944 | return rnew  | 
            ||
| 945 | |||
| 946 | def reorient_roi(self, ref1_coords, ref2_coords, ref3_coords, ref1_newpos, ref2_newpos, ref3_newpos):  | 
            ||
| 947 | """ Move and rotate the ROI to a new position specified by the newpos of 3 reference POIs from the saved ROI.  | 
            ||
| 948 | |||
| 949 | @param ref1_coords: coordinates (from ROI save file) of reference 1.  | 
            ||
| 950 | |||
| 951 | @param ref2_coords: similar, ref2.  | 
            ||
| 952 | |||
| 953 | @param ref3_coords: similar, ref3.  | 
            ||
| 954 | |||
| 955 | @param ref1_newpos: the new (current) position of POI reference 1.  | 
            ||
| 956 | |||
| 957 | @param ref2_newpos: similar, ref2.  | 
            ||
| 958 | |||
| 959 | @param ref3_newpos: similar, ref3.  | 
            ||
| 960 | """  | 
            ||
| 961 | |||
| 962 | for poikey in self.get_all_pois(abc_sort=True):  | 
            ||
| 963 | if poikey is not 'sample' and poikey is not 'crosshair':  | 
            ||
| 964 | thispoi = self.poi_list[poikey]  | 
            ||
| 965 | |||
| 966 | old_coords = thispoi.get_coords_in_sample()  | 
            ||
| 967 | |||
| 968 | new_coords = self.triangulate(old_coords, ref1_coords, ref2_coords, ref3_coords, ref1_newpos, ref2_newpos, ref3_newpos)  | 
            ||
| 969 | |||
| 970 | self.move_coords(poikey=poikey, newpos=new_coords)  | 
            ||
| 971 | |||
| 972 | def autofind_pois(self, neighborhood_size=1, min_threshold=10000, max_threshold=1e6):  | 
            ||
| 973 | """Automatically search the xy scan image for POIs.  | 
            ||
| 974 | |||
| 975 | @param neighborhood_size: size in microns. Only the brightest POI per neighborhood will be found.  | 
            ||
| 976 | |||
| 977 | @param min_threshold: POIs must have c/s above this threshold.  | 
            ||
| 978 | |||
| 979 | @param max_threshold: POIs must have c/s below this threshold.  | 
            ||
| 980 | """  | 
            ||
| 981 | |||
| 982 | # Calculate the neighborhood size in pixels from the image range and resolution  | 
            ||
| 983 | x_range_microns = np.max(self.roi_map_data[:, :, 0]) - np.min(self.roi_map_data[:, :, 0])  | 
            ||
| 984 | y_range_microns = np.max(self.roi_map_data[:, :, 1]) - np.min(self.roi_map_data[:, :, 1])  | 
            ||
| 985 | y_pixels = len(self.roi_map_data)  | 
            ||
| 986 | x_pixels = len(self.roi_map_data[1, :])  | 
            ||
| 987 | |||
| 988 | pixels_per_micron = np.max([x_pixels, y_pixels]) / np.max([x_range_microns, y_range_microns])  | 
            ||
| 989 | # The neighborhood in pixels is nbhd_size * pixels_per_um, but it must be 1 or greater  | 
            ||
| 990 | neighborhood_pix = int(np.max([math.ceil(pixels_per_micron * neighborhood_size), 1]))  | 
            ||
| 991 | |||
| 992 | data = self.roi_map_data[:, :, 3]  | 
            ||
| 993 | |||
| 994 | data_max = filters.maximum_filter(data, neighborhood_pix)  | 
            ||
| 995 | maxima = (data == data_max)  | 
            ||
| 996 | data_min = filters.minimum_filter(data, 3 * neighborhood_pix)  | 
            ||
| 997 | diff = ((data_max - data_min) > min_threshold)  | 
            ||
| 998 | maxima[diff is False] = 0  | 
            ||
| 999 | |||
| 1000 | labeled, num_objects = ndimage.label(maxima)  | 
            ||
| 1001 | xy = np.array(ndimage.center_of_mass(data, labeled, range(1, num_objects + 1)))  | 
            ||
| 1002 | |||
| 1003 | for count, pix_pos in enumerate(xy):  | 
            ||
| 1004 | poi_pos = self.roi_map_data[pix_pos[0], pix_pos[1], :][0:3]  | 
            ||
| 1005 | this_poi_key = self.add_poi(position=poi_pos, emit_change=False)  | 
            ||
| 1006 | self.rename_poi(poikey=this_poi_key, name='spot' + str(count), emit_change=False)  | 
            ||
| 1007 | |||
| 1008 | # Now that all the POIs are created, emit the signal for other things (ie gui) to update  | 
            ||
| 1009 | self.signal_poi_updated.emit()  | 
            ||
| 1010 |