Coverage for python/lsst/display/firefly/firefly.py: 12%

297 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-29 08:25 +0000

1# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010, 2015 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23import logging 

24from io import BytesIO 

25from socket import gaierror 

26 

27import lsst.afw.display.interface as interface 

28import lsst.afw.display.virtualDevice as virtualDevice 

29import lsst.afw.display.ds9Regions as ds9Regions 

30import lsst.afw.display as afwDisplay 

31import lsst.afw.math as afwMath 

32 

33from .footprints import createFootprintsTable 

34 

35try: 

36 import firefly_client 

37 _fireflyClient = None 

38except ImportError as e: 

39 raise RuntimeError(f"Cannot import firefly_client: {e}") 

40from ws4py.client import HandshakeError 

41 

42_LOG = logging.getLogger(__name__) 

43 

44 

45class FireflyError(Exception): 

46 

47 def __init__(self, str): 

48 Exception.__init__(self, str) 

49 

50 

51def firefly_version(): 

52 """Return the version of firefly_client in use, as a string""" 

53 return firefly_client.__version__ 

54 

55 

56class DisplayImpl(virtualDevice.DisplayImpl): 

57 """Device to talk to a firefly display""" 

58 

59 @staticmethod 

60 def _scoped_mask_id(frame, plane_name): 

61 """Build a server-side mask layer id that is unique per frame. 

62 

63 The Firefly server keys mask overlays by ``mask_id`` alone, so two 

64 frames that both register a layer named e.g. ``"DETECTED"`` would 

65 overwrite each other. Prefixing the plane name with the frame number 

66 keeps each frame's layers independent while remaining human-readable 

67 in Firefly's layer panel. 

68 """ 

69 return f"f{frame}__{plane_name}" 

70 

71 @staticmethod 

72 def __handleCallbacks(event): 

73 if 'type' in event['data']: 

74 if event['data']['type'] == 'AREA_SELECT': 

75 _LOG.debug('*************area select') 

76 pParams = {'URL': 'http://web.ipac.caltech.edu/staff/roby/demo/wise-m51-band2.fits', 

77 'ColorTable': '9'} 

78 plot_id = 3 

79 _fireflyClient.show_fits_image(fileOnServer=None, plot_id=plot_id, additionalParams=pParams) 

80 

81 _LOG.debug("Callback event info: %s", event) 

82 return 

83 data = dict(_.split('=') for _ in event.get('data', {}).split('&')) 

84 if data.get('type') == "POINT": 

85 _LOG.debug("Event Received: %s", data.get('id')) 

86 

87 def __init__(self, display, verbose=False, url=None, 

88 name=None, *args, **kwargs): 

89 virtualDevice.DisplayImpl.__init__(self, display, verbose) 

90 

91 if self.verbose: 

92 print("Opening firefly device %s" % (self.display.frame if self.display else "[None]")) 

93 

94 global _fireflyClient 

95 if not _fireflyClient: 

96 import os 

97 start_tab = None 

98 html_file = kwargs.get('html_file', 

99 os.environ.get('FIREFLY_HTML', '')) 

100 if url is None: 

101 if (('fireflyLabExtension' in os.environ) and 

102 ('fireflyURLLab' in os.environ)): 

103 url = os.environ['fireflyURLLab'] 

104 start_tab = kwargs.get('start_tab', True) 

105 start_browser_tab = kwargs.get('start_browser_tab', False) 

106 if (name is None) and ('fireflyChannelLab' in os.environ): 

107 name = os.environ['fireflyChannelLab'] 

108 elif 'FIREFLY_URL' in os.environ: 

109 url = os.environ['FIREFLY_URL'] 

110 else: 

111 raise RuntimeError('Cannot determine url from environment; you must pass url') 

112 

113 token = kwargs.get('token', 

114 os.environ.get('ACCESS_TOKEN', None)) 

115 

116 try: 

117 if start_tab: 

118 if verbose: 

119 print('Starting Jupyterlab client') 

120 _fireflyClient = firefly_client.FireflyClient.make_lab_client( 

121 start_tab=True, start_browser_tab=start_browser_tab, 

122 html_file=html_file, verbose=verbose, 

123 token=token) 

124 

125 else: 

126 if verbose: 

127 print('Starting vanilla client') 

128 _fireflyClient = firefly_client.FireflyClient.make_client( 

129 url=url, html_file=html_file, launch_browser=True, 

130 channel_override=name, verbose=verbose, 

131 token=token) 

132 

133 except (HandshakeError, gaierror) as e: 

134 raise RuntimeError(f"Unable to connect to {url or ''}: {e}") 

135 

136 try: 

137 _fireflyClient.add_listener(self.__handleCallbacks) 

138 except Exception as e: 

139 raise RuntimeError("Cannot add listener. Browser must be connected" 

140 f"to {_fireflyClient.get_firefly_url()}: {e}") 

141 

142 self._isBuffered = False 

143 self._regions = [] 

144 self._regionLayerId = self._getRegionLayerId() 

145 self._fireflyFitsID = None 

146 self._fireflyMaskOnServer = None 

147 self._client = _fireflyClient 

148 self._channel = _fireflyClient.channel 

149 self._url = _fireflyClient.get_firefly_url() 

150 self._maskIds = [] 

151 self._maskDict = {} 

152 self._maskPlaneColors = {} 

153 self._maskTransparencies = {} 

154 self._lastZoom = None 

155 self._lastPan = None 

156 self._lastStretch = None 

157 

158 def _getRegionLayerId(self): 

159 return f"lsstRegions{self.display.frame}" if self.display else "None" 

160 

161 def _clearImage(self): 

162 """Delete the current image in the Firefly viewer 

163 """ 

164 self._client.dispatch(action_type='ImagePlotCntlr.deletePlotView', 

165 payload=dict(plotId=str(self.display.frame))) 

166 

167 def _mtv(self, image, mask=None, wcs=None, title="", metadata=None): 

168 """Display an Image and/or Mask on a Firefly display 

169 """ 

170 if title == "": 

171 title = str(self.display.frame) 

172 if image: 

173 if self.verbose: 

174 print('displaying image') 

175 self._erase() 

176 

177 with BytesIO() as fd: 

178 afwDisplay.writeFitsImage(fd, image, wcs, title, metadata=metadata) 

179 fd.seek(0, 0) 

180 self._fireflyFitsID = _fireflyClient.upload_fits_data(fd) 

181 

182 try: 

183 viewer_id = f'image-{_fireflyClient.render_tree_id}-{self.frame}' 

184 except AttributeError: 

185 viewer_id = f'image-{self.frame}' 

186 extraParams = dict(Title=title, 

187 MultiImageIdx=0, 

188 PredefinedOverlayIds=' ', 

189 viewer_id=viewer_id) 

190 # Firefly's Javascript API requires a space for parameters; 

191 # otherwise the parameter will be ignored 

192 

193 if self._lastZoom: 

194 extraParams['InitZoomLevel'] = self._lastZoom 

195 extraParams['ZoomType'] = 'LEVEL' 

196 if self._lastPan: 

197 extraParams['InitialCenterPosition'] = f'{self._lastPan[0]:.3f};{self._lastPan[1]:.3f};PIXEL' 

198 if self._lastStretch: 

199 extraParams['RangeValues'] = self._lastStretch 

200 

201 ret = _fireflyClient.show_fits_image(self._fireflyFitsID, plot_id=str(self.display.frame), 

202 **extraParams) 

203 

204 if not ret["success"]: 

205 raise RuntimeError("Display of image failed") 

206 

207 if mask: 

208 if self.verbose: 

209 print('displaying mask') 

210 with BytesIO() as fdm: 

211 afwDisplay.writeFitsImage(fdm, mask, wcs, title, metadata=metadata) 

212 fdm.seek(0, 0) 

213 self._fireflyMaskOnServer = _fireflyClient.upload_fits_data(fdm) 

214 

215 maskPlaneDict = mask.getMaskPlaneDict() 

216 for k, v in maskPlaneDict.items(): 

217 self._maskDict[k] = v 

218 self._maskPlaneColors[k] = self.display.getMaskPlaneColor(k) 

219 usedPlanes = int(afwMath.makeStatistics(mask, afwMath.SUM).getValue()) 

220 for k in self._maskDict: 

221 if (((1 << self._maskDict[k]) & usedPlanes) and 

222 (k in self._maskPlaneColors) and 

223 (self._maskPlaneColors[k] is not None) and 

224 (self._maskPlaneColors[k].lower() != 'ignore')): 

225 _fireflyClient.add_mask(bit_number=self._maskDict[k], 

226 image_number=0, 

227 plot_id=str(self.display.frame), 

228 mask_id=self._scoped_mask_id(self.display.frame, k), 

229 title=k + ' - bit %d'%self._maskDict[k], 

230 color=self._maskPlaneColors[k], 

231 file_on_server=self._fireflyMaskOnServer) 

232 if k in self._maskTransparencies: 

233 self._setMaskTransparency(self._maskTransparencies[k], k) 

234 self._maskIds.append((self.display.frame, k)) 

235 

236 def _remove_masks(self): 

237 """Remove mask layers for the current frame. 

238 

239 Layers belonging to other frames are left in place; otherwise 

240 loading a new image in one frame would clear the mask overlays 

241 of every other displayed frame. 

242 """ 

243 frame = self.display.frame 

244 kept = [] 

245 for f, name in self._maskIds: 

246 if f == frame: 

247 _fireflyClient.remove_mask(plot_id=str(frame), 

248 mask_id=self._scoped_mask_id(f, name)) 

249 else: 

250 kept.append((f, name)) 

251 self._maskIds = kept 

252 

253 def _buffer(self, enable=True): 

254 """!Enable or disable buffering of writes to the display 

255 param enable True or False, as appropriate 

256 """ 

257 self._isBuffered = enable 

258 

259 def _flush(self): 

260 """!Flush any I/O buffers 

261 """ 

262 if not self._regions: 

263 return 

264 

265 if self.verbose: 

266 print("Flushing %d regions" % len(self._regions)) 

267 print(self._regions) 

268 

269 self._regionLayerId = self._getRegionLayerId() 

270 _fireflyClient.add_region_data(region_data=self._regions, plot_id=str(self.display.frame), 

271 region_layer_id=self._regionLayerId) 

272 self._regions = [] 

273 

274 def _uploadTextData(self, regions): 

275 self._regions += regions 

276 

277 if not self._isBuffered: 

278 self._flush() 

279 

280 def _close(self): 

281 """Called when the device is closed""" 

282 if self.verbose: 

283 print("Closing firefly device %s" % (self.display.frame if self.display else "[None]")) 

284 if _fireflyClient is not None: 

285 _fireflyClient.disconnect() 

286 _fireflyClient.session.close() 

287 

288 def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None): 

289 """Draw a symbol onto the specified DS9 frame at (col,row) = (c,r) [0-based coordinates] 

290 Possible values are: 

291 + Draw a + 

292 x Draw an x 

293 * Draw a * 

294 o Draw a circle 

295 @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored) 

296 An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored) 

297 Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended 

298 with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle 

299 is ignored otherwise). 

300 

301 N.b. objects derived from BaseCore include Axes and Quadrupole. 

302 """ 

303 self._uploadTextData(ds9Regions.dot(symb, c, r, size, ctype, fontFamily, textAngle)) 

304 

305 def _drawLines(self, points, ctype): 

306 """Connect the points, a list of (col,row) 

307 Ctype is the name of a colour (e.g. 'red')""" 

308 

309 self._uploadTextData(ds9Regions.drawLines(points, ctype)) 

310 

311 def _erase(self): 

312 """Erase all overlays on the image""" 

313 if self.verbose: 

314 print(f'region layer id is {self._regionLayerId}') 

315 if self._regionLayerId: 

316 _fireflyClient.delete_region_layer(self._regionLayerId, plot_id=str(self.display.frame)) 

317 

318 def _setCallback(self, what, func): 

319 if func != interface.noop_callback: 

320 try: 

321 status = _fireflyClient.add_extension('POINT' if False else 'AREA_SELECT', title=what, 

322 plot_id=str(self.display.frame), 

323 extension_id=what) 

324 if not status['success']: 

325 pass 

326 except Exception as e: 

327 raise RuntimeError("Cannot set callback. Browser must be (re)opened " 

328 f"to {_fireflyClient.url_bw}{_fireflyClient.channel} : {e}") 

329 

330 def _getEvent(self): 

331 """Return an event generated by a keypress or mouse click 

332 """ 

333 ev = interface.Event("q") 

334 

335 if self.verbose: 

336 print(f"virtual[{self.display.frame}]._getEvent() -> {ev}") 

337 

338 return ev 

339 # 

340 # Set gray scale 

341 # 

342 

343 def _scale(self, algorithm, min, max, unit=None, *args, **kwargs): 

344 """Scale the image stretch and limits 

345 

346 Parameters: 

347 ----------- 

348 algorithm : `str` 

349 stretch algorithm, e.g. 'linear', 'log', 'loglog', 'equal', 'squared', 

350 'sqrt', 'asinh', powerlaw_gamma' 

351 min : `float` or `str` 

352 lower limit, or 'minmax' for full range, or 'zscale' 

353 max : `float` or `str` 

354 upper limit; overrriden if min is 'minmax' or 'zscale' 

355 unit : `str` 

356 unit for min and max. 'percent', 'absolute', 'sigma'. 

357 if not specified, min and max are presumed to be in 'absolute' units. 

358 

359 *args, **kwargs : additional position and keyword arguments. 

360 The options are shown below: 

361 

362 **Q** : `float`, optional 

363 The asinh softening parameter for asinh stretch. 

364 Use Q=0 for linear stretch, increase Q to make brighter features visible. 

365 When not specified or None, Q is calculated by Firefly to use full color range. 

366 **gamma** 

367 The gamma value for power law gamma stretch (default 2.0) 

368 **zscale_contrast** : `int`, optional 

369 Contrast parameter in percent for zscale algorithm (default 25) 

370 **zscale_samples** : `int`, optional 

371 Number of samples for zscale algorithm (default 600) 

372 **zscale_samples_perline** : `int`, optional 

373 Number of samples per line for zscale algorithm (default 120) 

374 """ 

375 stretch_algorithms = ('linear', 'log', 'loglog', 'equal', 'squared', 'sqrt', 

376 'asinh', 'powerlaw_gamma') 

377 interval_methods = ('percent', 'maxmin', 'absolute', 'zscale', 'sigma') 

378 # 

379 # 

380 # Normalise algorithm's case 

381 # 

382 if algorithm: 

383 algorithm = dict((a.lower(), a) for a in stretch_algorithms).get(algorithm.lower(), algorithm) 

384 

385 if algorithm not in stretch_algorithms: 

386 raise FireflyError( 

387 'Algorithm {} is invalid; please choose one of "{}"'.format( 

388 algorithm, '", "'.join(stretch_algorithms) 

389 ) 

390 ) 

391 self._stretchAlgorithm = algorithm 

392 else: 

393 algorithm = 'linear' 

394 

395 # Translate parameters for asinh and powerlaw_gamma stretches 

396 if 'Q' in kwargs: 

397 kwargs['asinh_q_value'] = kwargs['Q'] 

398 del kwargs['Q'] 

399 

400 if 'gamma' in kwargs: 

401 kwargs['gamma_value'] = kwargs['gamma'] 

402 del kwargs['gamma'] 

403 

404 if min == 'minmax': 

405 interval_type = 'percent' 

406 unit = 'percent' 

407 min, max = 0, 100 

408 elif min == 'zscale': 

409 interval_type = 'zscale' 

410 else: 

411 interval_type = None 

412 

413 if not unit: 

414 unit = 'absolute' 

415 

416 units = ('percent', 'absolute', 'sigma') 

417 if unit not in units: 

418 raise FireflyError( 

419 'Unit {} is invalid; please choose one of "{}"'.format(unit, '", "'.join(units)) 

420 ) 

421 

422 if unit == 'sigma': 

423 interval_type = 'sigma' 

424 elif unit == 'absolute' and interval_type is None: 

425 interval_type = 'absolute' 

426 elif unit == 'percent': 

427 interval_type = 'percent' 

428 

429 self._stretchMin = min 

430 self._stretchMax = max 

431 self._stretchUnit = unit 

432 

433 if interval_type not in interval_methods: 

434 raise FireflyError(f'Interval method {interval_type} is invalid') 

435 

436 rval = {} 

437 if interval_type != 'zscale': 

438 rval = _fireflyClient.set_stretch(str(self.display.frame), stype=interval_type, 

439 algorithm=algorithm, lower_value=min, 

440 upper_value=max, **kwargs) 

441 else: 

442 if 'zscale_contrast' not in kwargs: 

443 kwargs['zscale_contrast'] = 25 

444 if 'zscale_samples' not in kwargs: 

445 kwargs['zscale_samples'] = 600 

446 if 'zscale_samples_perline' not in kwargs: 

447 kwargs['zscale_samples_perline'] = 120 

448 rval = _fireflyClient.set_stretch(str(self.display.frame), stype='zscale', 

449 algorithm=algorithm, **kwargs) 

450 

451 if 'rv_string' in rval: 

452 self._lastStretch = rval['rv_string'] 

453 

454 def _setMaskTransparency(self, transparency, maskName): 

455 """Specify mask transparency (percent); or None to not set it when loading masks. 

456 

457 Operates on the current frame. ``_maskIds`` holds ``(frame, name)`` 

458 tuples (recorded by ``_mtv``); we filter those to the current frame. 

459 Plane names from ``_defaultMaskPlaneColor`` are also included, scoped 

460 to the current frame. 

461 """ 

462 frame = self.display.frame 

463 if maskName is not None: 

464 names = [maskName] 

465 else: 

466 names = {name for f, name in self._maskIds if f == frame} 

467 names.update(self.display._defaultMaskPlaneColor.keys()) 

468 for k in names: 

469 self._maskTransparencies[k] = transparency 

470 _fireflyClient.dispatch(action_type='ImagePlotCntlr.overlayPlotChangeAttributes', 

471 payload={'plotId': str(frame), 

472 'imageOverlayId': self._scoped_mask_id(frame, k), 

473 'attributes': {'opacity': 1.0 - transparency/100.}, 

474 'doReplot': False}) 

475 

476 def _getMaskTransparency(self, maskName): 

477 """Return the current mask's transparency""" 

478 transparency = None 

479 if maskName in self._maskTransparencies: 

480 transparency = self._maskTransparencies[maskName] 

481 return transparency 

482 

483 def _setMaskPlaneColor(self, maskName, color): 

484 """Specify mask color for the current frame. 

485 """ 

486 frame = self.display.frame 

487 scoped_id = self._scoped_mask_id(frame, maskName) 

488 _fireflyClient.remove_mask(plot_id=str(frame), mask_id=scoped_id) 

489 self._maskPlaneColors[maskName] = color 

490 if (color.lower() != 'ignore'): 

491 _fireflyClient.add_mask(bit_number=self._maskDict[maskName], 

492 image_number=1, 

493 plot_id=str(frame), 

494 mask_id=scoped_id, 

495 color=self.display.getMaskPlaneColor(maskName), 

496 file_on_server=self._fireflyFitsID) 

497 

498 def _show(self): 

499 """Show the requested window""" 

500 if self._client.render_tree_id is not None: 

501 # we are using Jupyterlab 

502 self._client.dispatch(self._client.ACTION_DICT['StartLabWindow'], 

503 {}) 

504 else: 

505 localbrowser, url = _fireflyClient.launch_browser(verbose=self.verbose) 

506 if not localbrowser and not self.verbose: 

507 _fireflyClient.display_url() 

508 

509 # 

510 # Zoom and Pan 

511 # 

512 

513 def _zoom(self, zoomfac): 

514 """Zoom display by specified amount 

515 

516 Parameters: 

517 ----------- 

518 zoomfac: `float` 

519 zoom level in screen pixels per image pixel 

520 """ 

521 self._lastZoom = zoomfac 

522 _fireflyClient.set_zoom(plot_id=str(self.display.frame), factor=zoomfac) 

523 

524 def _pan(self, colc, rowc): 

525 """Pan to specified pixel coordinates 

526 

527 Parameters: 

528 ----------- 

529 colc, rowc : `float` 

530 column and row in units of pixels (zero-based convention, 

531 with the xy0 already subtracted off) 

532 """ 

533 self._lastPan = [colc+0.5, rowc+0.5] # saved for future use in _mtv 

534 # Firefly's internal convention is first pixel is (0.5, 0.5) 

535 _fireflyClient.set_pan(plot_id=str(self.display.frame), x=colc, y=rowc) 

536 

537 # Extensions to the API that are specific to using the Firefly backend 

538 

539 def getClient(self): 

540 """Get the instance of FireflyClient for this display 

541 

542 Returns: 

543 -------- 

544 `firefly_client.FireflyClient` 

545 Instance of FireflyClient used by this display 

546 """ 

547 return self._client 

548 

549 def clearViewer(self): 

550 """Reinitialize the viewer 

551 """ 

552 self._client.reinit_viewer() 

553 

554 def resetLayout(self): 

555 """Reset the layout of the Firefly Slate browser 

556 

557 Clears the display and adds Slate cells to display image in upper left, 

558 plot area in upper right, and plots stretch across the bottom 

559 """ 

560 self.clearViewer() 

561 try: 

562 tables_cell_id = 'tables-' + str(_fireflyClient.render_tree_id) 

563 except AttributeError: 

564 tables_cell_id = 'tables' 

565 self._client.add_cell(row=2, col=0, width=4, height=2, element_type='tables', 

566 cell_id=tables_cell_id) 

567 try: 

568 image_cell_id = ('image-' + str(_fireflyClient.render_tree_id) + '-' + 

569 str(self.frame)) 

570 except AttributeError: 

571 image_cell_id = 'image-' + str(self.frame) 

572 self._client.add_cell(row=0, col=0, width=2, height=3, element_type='images', 

573 cell_id=image_cell_id) 

574 try: 

575 plots_cell_id = 'plots-' + str(_fireflyClient.render_tree_id) 

576 except AttributeError: 

577 plots_cell_id = 'plots' 

578 self._client.add_cell(row=0, col=2, width=2, height=3, element_type='xyPlots', 

579 cell_id=plots_cell_id) 

580 

581 def overlayFootprints(self, catalog, color='rgba(74,144,226,0.60)', 

582 highlightColor='cyan', selectColor='orange', 

583 style='fill', layerString='detection footprints ', 

584 titleString='catalog footprints '): 

585 """Overlay outlines of footprints from a catalog 

586 

587 Overlay outlines of LSST footprints from the input catalog. The colors 

588 and style can be specified as parameters, and the base color and style 

589 can be changed in the Firefly browser user interface. 

590 

591 Parameters: 

592 ----------- 

593 catalog : `lsst.afw.table.SourceCatalog` 

594 Source catalog from which to display footprints. 

595 color : `str` 

596 Color for footprints overlay. Colors can be specified as a name 

597 like 'cyan' or afwDisplay.RED; as an rgb value such as 

598 'rgb(80,100,220)'; or as rgb plus alpha (transparency) such 

599 as 'rgba('74,144,226,0.60)'. 

600 highlightColor : `str` 

601 Color for highlighted footprints 

602 selectColor : `str` 

603 Color for selected footprints 

604 style : {'fill', 'outline'} 

605 Style of footprints display, filled or outline 

606 insertColumn : `int` 

607 Column at which to insert the "family_id" and "category" columns 

608 layerString: `str` 

609 Name of footprints layer string, to concatenate with the frame 

610 Re-using the layer_string will overwrite the previous table and 

611 footprints 

612 titleString: `str` 

613 Title of catalog, to concatenate with the frame 

614 """ 

615 footprintTable = createFootprintsTable(catalog) 

616 with BytesIO() as fd: 

617 footprintTable.to_xml(fd) 

618 tableval = self._client.upload_data(fd, 'UNKNOWN') 

619 self._client.overlay_footprints(footprint_file=tableval, 

620 title=titleString + str(self.display.frame), 

621 footprint_layer_id=layerString + str(self.display.frame), 

622 plot_id=str(self.display.frame), 

623 color=color, 

624 highlightColor=highlightColor, 

625 selectColor=selectColor, 

626 style=style) 

627 

628 def alignImages(self, match_type="Standard", lock_match=True): 

629 """Align and optionally lock the orientation of the images being 

630 displayed. 

631 

632 See the Firefly native docs for additional kwargs reference: 

633 https://caltech-ipac.github.io/firefly_client/api/firefly_client.FireflyClient.html#firefly_client.FireflyClient.align_images 

634 

635 Parameters 

636 ---------- 

637 match_type : `str`, optional 

638 Match type to use to align the images: align by WCS (‘Standard’), 

639 by target (‘Target’), by pixel prigins (‘Pixel’), and by pixel at 

640 image centers (‘PixelCenter’). 

641 lock_match : `bool`, optional 

642 Whether to lock the alignment. Panning/zooming in one image will 

643 preserve the alignment in other images. 

644 

645 Returns 

646 ------- 

647 out : `dict` 

648 Status of the request. 

649 

650 Raises 

651 ------ 

652 ValueError 

653 Raised if match_type is not one of the allowed values. 

654 """ 

655 types = {"Standard", "Target", "Pixel", "PixelCenter"} 

656 if match_type not in types: 

657 raise ValueError(f"match_type={match_type} not allowed from expected types: {types}.") 

658 

659 return self._client.align_images(match_type=match_type, lock_match=lock_match)