cubevis 0.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. cubevis/LICENSE.rst +500 -0
  2. cubevis/__icons__/20px/fast-backward.svg +13 -0
  3. cubevis/__icons__/20px/fast-forward.svg +13 -0
  4. cubevis/__icons__/20px/step-backward.svg +12 -0
  5. cubevis/__icons__/20px/step-forward.svg +12 -0
  6. cubevis/__icons__/add-chan.png +0 -0
  7. cubevis/__icons__/add-chan.svg +84 -0
  8. cubevis/__icons__/add-cube.png +0 -0
  9. cubevis/__icons__/add-cube.svg +186 -0
  10. cubevis/__icons__/drag.png +0 -0
  11. cubevis/__icons__/drag.svg +109 -0
  12. cubevis/__icons__/mask-selected.png +0 -0
  13. cubevis/__icons__/mask.png +0 -0
  14. cubevis/__icons__/mask.svg +1 -0
  15. cubevis/__icons__/new-layer-sm-selected.png +0 -0
  16. cubevis/__icons__/new-layer-sm-selected.svg +88 -0
  17. cubevis/__icons__/new-layer-sm.png +0 -0
  18. cubevis/__icons__/new-layer-sm.svg +15 -0
  19. cubevis/__icons__/reset.png +0 -0
  20. cubevis/__icons__/reset.svg +11 -0
  21. cubevis/__icons__/sub-chan.png +0 -0
  22. cubevis/__icons__/sub-chan.svg +71 -0
  23. cubevis/__icons__/sub-cube.png +0 -0
  24. cubevis/__icons__/sub-cube.svg +95 -0
  25. cubevis/__icons__/zoom-to-fit.png +0 -0
  26. cubevis/__icons__/zoom-to-fit.svg +21 -0
  27. cubevis/__init__.py +58 -0
  28. cubevis/__js__/bokeh-3.6.1.min.js +728 -0
  29. cubevis/__js__/bokeh-tables-3.6.1.min.js +119 -0
  30. cubevis/__js__/bokeh-widgets-3.6.1.min.js +141 -0
  31. cubevis/__js__/casalib.min.js +1 -0
  32. cubevis/__js__/cubevisjs.min.js +62 -0
  33. cubevis/__version__.py +1 -0
  34. cubevis/apps/__init__.py +44 -0
  35. cubevis/apps/_createmask.py +461 -0
  36. cubevis/apps/_createregion.py +513 -0
  37. cubevis/apps/_interactiveclean.py +3260 -0
  38. cubevis/apps/_interactiveclean_wrappers.py +130 -0
  39. cubevis/apps/_ms_raster.py +815 -0
  40. cubevis/apps/_plotants.py +286 -0
  41. cubevis/apps/_plotbandpass.py +7 -0
  42. cubevis/bokeh/__init__.py +29 -0
  43. cubevis/bokeh/annotations/__init__.py +1 -0
  44. cubevis/bokeh/annotations/_ev_poly_annotation.py +6 -0
  45. cubevis/bokeh/components/__init__.py +28 -0
  46. cubevis/bokeh/format/__init__.py +31 -0
  47. cubevis/bokeh/format/_time_ticks.py +44 -0
  48. cubevis/bokeh/format/_wcs_ticks.py +45 -0
  49. cubevis/bokeh/models/__init__.py +4 -0
  50. cubevis/bokeh/models/_edit_span.py +7 -0
  51. cubevis/bokeh/models/_ev_text_input.py +6 -0
  52. cubevis/bokeh/models/_tip.py +37 -0
  53. cubevis/bokeh/models/_tip_button.py +50 -0
  54. cubevis/bokeh/sources/__init__.py +35 -0
  55. cubevis/bokeh/sources/_data_pipe.py +258 -0
  56. cubevis/bokeh/sources/_image_data_source.py +83 -0
  57. cubevis/bokeh/sources/_image_pipe.py +581 -0
  58. cubevis/bokeh/sources/_spectra_data_source.py +55 -0
  59. cubevis/bokeh/sources/_updatable_data_source.py +189 -0
  60. cubevis/bokeh/state/__init__.py +34 -0
  61. cubevis/bokeh/state/_initialize.py +164 -0
  62. cubevis/bokeh/state/_javascript.py +53 -0
  63. cubevis/bokeh/state/_palette.py +58 -0
  64. cubevis/bokeh/state/_session.py +44 -0
  65. cubevis/bokeh/state/js/bokeh-2.4.1.min.js +596 -0
  66. cubevis/bokeh/state/js/bokeh-gl-2.4.1.min.js +74 -0
  67. cubevis/bokeh/state/js/bokeh-tables-2.4.1.min.js +132 -0
  68. cubevis/bokeh/state/js/bokeh-widgets-2.4.1.min.js +118 -0
  69. cubevis/bokeh/state/js/casaguijs-v0.0.4.0-b2.4.min.js +49 -0
  70. cubevis/bokeh/state/js/casaguijs-v0.0.5.0-b2.4.min.js +49 -0
  71. cubevis/bokeh/state/js/casaguijs-v0.0.6.0-b2.4.min.js +49 -0
  72. cubevis/bokeh/state/js/casalib-v0.0.1.min.js +1 -0
  73. cubevis/bokeh/tools/__init__.py +31 -0
  74. cubevis/bokeh/tools/_cbreset_tool.py +52 -0
  75. cubevis/bokeh/tools/_drag_tool.py +61 -0
  76. cubevis/bokeh/utils/__init__.py +35 -0
  77. cubevis/bokeh/utils/_axes_labels.py +94 -0
  78. cubevis/bokeh/utils/_svg_icon.py +136 -0
  79. cubevis/data/__init__.py +1 -0
  80. cubevis/data/casaimage/__init__.py +114 -0
  81. cubevis/data/measurement_set/__init__.py +7 -0
  82. cubevis/data/measurement_set/_ms_data.py +178 -0
  83. cubevis/data/measurement_set/processing_set/__init__.py +30 -0
  84. cubevis/data/measurement_set/processing_set/_ps_concat.py +98 -0
  85. cubevis/data/measurement_set/processing_set/_ps_coords.py +78 -0
  86. cubevis/data/measurement_set/processing_set/_ps_data.py +213 -0
  87. cubevis/data/measurement_set/processing_set/_ps_io.py +55 -0
  88. cubevis/data/measurement_set/processing_set/_ps_raster_data.py +154 -0
  89. cubevis/data/measurement_set/processing_set/_ps_select.py +91 -0
  90. cubevis/data/measurement_set/processing_set/_ps_stats.py +218 -0
  91. cubevis/data/measurement_set/processing_set/_xds_data.py +149 -0
  92. cubevis/plot/__init__.py +1 -0
  93. cubevis/plot/ms_plot/__init__.py +29 -0
  94. cubevis/plot/ms_plot/_ms_plot.py +242 -0
  95. cubevis/plot/ms_plot/_ms_plot_constants.py +22 -0
  96. cubevis/plot/ms_plot/_ms_plot_selectors.py +348 -0
  97. cubevis/plot/ms_plot/_raster_plot.py +292 -0
  98. cubevis/plot/ms_plot/_raster_plot_inputs.py +116 -0
  99. cubevis/plot/ms_plot/_xds_plot_axes.py +110 -0
  100. cubevis/private/__java__/xml-casa-assembly-1.86.jar +0 -0
  101. cubevis/private/_gclean.py +798 -0
  102. cubevis/private/casashell/createmask.py +332 -0
  103. cubevis/private/casashell/iclean.py +4432 -0
  104. cubevis/private/casatasks/__init__.py +140 -0
  105. cubevis/private/casatasks/createmask.py +86 -0
  106. cubevis/private/casatasks/createregion.py +83 -0
  107. cubevis/private/casatasks/iclean.py +1831 -0
  108. cubevis/readme.rst +16 -0
  109. cubevis/remote/__init__.py +10 -0
  110. cubevis/remote/_gclean.py +61 -0
  111. cubevis/remote/_local.py +287 -0
  112. cubevis/remote/_remote_kernel.py +80 -0
  113. cubevis/toolbox/__init__.py +32 -0
  114. cubevis/toolbox/_app_context.py +74 -0
  115. cubevis/toolbox/_cube.py +3457 -0
  116. cubevis/toolbox/_region_list.py +197 -0
  117. cubevis/utils/_ResourceManager.py +86 -0
  118. cubevis/utils/__init__.py +620 -0
  119. cubevis/utils/_contextmgrchain.py +84 -0
  120. cubevis/utils/_conversion.py +93 -0
  121. cubevis/utils/_copydoc.py +55 -0
  122. cubevis/utils/_docenum.py +25 -0
  123. cubevis/utils/_import_protected_module.py +35 -0
  124. cubevis/utils/_logging.py +85 -0
  125. cubevis/utils/_pkgs.py +77 -0
  126. cubevis/utils/_regions.py +40 -0
  127. cubevis/utils/_static.py +66 -0
  128. cubevis/utils/_tiles.py +167 -0
  129. cubevis-0.5.2.dist-info/METADATA +151 -0
  130. cubevis-0.5.2.dist-info/RECORD +132 -0
  131. cubevis-0.5.2.dist-info/WHEEL +4 -0
  132. cubevis-0.5.2.dist-info/licenses/LICENSE +504 -0
@@ -0,0 +1,461 @@
1
+ ########################################################################
2
+ #
3
+ # Copyright (C) 2022, 2024
4
+ # Associated Universities, Inc. Washington DC, USA.
5
+ #
6
+ # This script is free software; you can redistribute it and/or modify it
7
+ # under the terms of the GNU Library General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or (at your
9
+ # option) any later version.
10
+ #
11
+ # This library is distributed in the hope that it will be useful, but WITHOUT
12
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
14
+ # License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Library General Public License
17
+ # along with this library; if not, write to the Free Software Foundation,
18
+ # Inc., 675 Massachusetts Ave, Cambridge, MA 02139, USA.
19
+ #
20
+ # Correspondence concerning AIPS++ should be adressed as follows:
21
+ # Internet email: casa-feedback@nrao.edu.
22
+ # Postal address: AIPS++ Project Office
23
+ # National Radio Astronomy Observatory
24
+ # 520 Edgemont Road
25
+ # Charlottesville, VA 22903-2475 USA
26
+ #
27
+ ########################################################################
28
+ '''implementation of the ``CreateMask`` application for interactive creation
29
+ of channel masks'''
30
+
31
+ from os.path import exists, splitext, join
32
+ from os.path import split as splitpath
33
+ import asyncio
34
+ from contextlib import asynccontextmanager
35
+ from bokeh.layouts import row, column
36
+ from bokeh.plotting import show
37
+ from bokeh.models import Button, CustomJS, TabPanel, Tabs, Spacer, Div
38
+ from cubevis.toolbox import CubeMask, AppContext
39
+ from cubevis.bokeh.utils import svg_icon
40
+ from bokeh.io import reset_output as reset_bokeh_output
41
+ from bokeh.io import output_notebook
42
+ from bokeh.models.dom import HTML
43
+ from bokeh.models.ui.tooltips import Tooltip
44
+ from ..utils import resource_manager, reset_resource_manager, is_notebook
45
+ from ..data import casaimage
46
+ from ..bokeh.models import TipButton, Tip
47
+ from ..utils import ContextMgrChain as CMC
48
+
49
+ class CreateMask:
50
+ '''Class that can be used to launch a createmask GUI with ``CreateMask('test.im','mask.im')( )``.
51
+ ``CreateMask`` operates in the same manner as ``InteractiveClean``. Regions drawn on the
52
+ displalyed CASA image cube can be added or subtracted from the related but separate mask
53
+ cube where each pixel is a 1 (masked) or a 0 (unmasked).
54
+ '''
55
+
56
+ def __stop( self ):
57
+ if not self.__result_future.done( ):
58
+ self.__result_future.set_result(self.__retrieve_result( ))
59
+
60
+ def _abort_handler( self, err ):
61
+ self._error_result = err
62
+ self.__stop( )
63
+
64
+ def __reset( self ):
65
+ if self.__initialized:
66
+ reset_bokeh_output( )
67
+ reset_resource_manager( )
68
+
69
+ ###
70
+ ### reset asyncio result future
71
+ ###
72
+ self.__result_future = None
73
+
74
+ ###
75
+ ### used by data pipe (websocket) initialization function
76
+ ###
77
+ self.__initialized = False
78
+
79
+ self._image = None
80
+ ###
81
+ ### error or exception result
82
+ ###
83
+ self._error_result = None
84
+
85
+ def __expand_mask_paths( self, path_pairs ):
86
+ '''return expanded mask paths
87
+
88
+ Parameters
89
+ ----------
90
+ path_pairs: list of tuples
91
+ each tuple contains a string in the first element that represents an image path
92
+ and the second element is either a None which signals that the an mask path should
93
+ be generated based upon the image path or a string which represents that mask path
94
+ for the first element image.
95
+
96
+ Returns
97
+ -------
98
+ [ (str,str) ]
99
+ A list of tuples is returned. The first element of each
100
+ tuple is a string that represents the image path and the second element is a string
101
+ which represents the mask path.
102
+ '''
103
+ uniquified_names = { } ### dict to keep track of repeated iamges
104
+
105
+ def create_mask_path( impath ):
106
+ if impath in uniquified_names and uniquified_names[impath][0] > 1:
107
+ uniq = "%032d" % uniquified_names[impath][1]
108
+ uniquified_names[impath][1] += 1
109
+ else:
110
+ uniq = ''
111
+ path,file = splitpath(impath)
112
+ if len(path) > 0 and not exists(path):
113
+ raise RuntimeError( f'''CreateMask: mask path '{path}' does not exist''' )
114
+ basename,_ = splitext(file)
115
+ return join( path, f'''{basename}{uniq}.mask''')
116
+
117
+ ### python zip etc can only be read once
118
+ path_pairs_ = list(path_pairs)
119
+ for p in path_pairs_:
120
+ if p[0] in uniquified_names:
121
+ uniquified_names[p[0]][0] += 1
122
+ else:
123
+ uniquified_names[p[0]] = [1,1]
124
+
125
+ return list( map( lambda p: p if p[1] is not None else (p[0],create_mask_path(p[0])), path_pairs_ ) )
126
+
127
+ def __create_masks( self, paths ):
128
+ '''Create missing masks...'''
129
+ ### Create any mask images which do not exist (if create=True)
130
+ return list( map( lambda p: (p[0],casaimage.new(*p)) if not exists(p[1]) else p, paths ) )
131
+
132
+
133
+ def __init__( self, image, mask=None, create=True ):
134
+ '''create a ``createmask`` object which will display image planes from a CASA
135
+ image and allow the user to draw masks for each channel.
136
+
137
+ Parameters
138
+ ----------
139
+ image: str or list of str
140
+ path(s) to CASA image for which interactive masks will be drawn
141
+
142
+ mask: str or list of str
143
+ path(s) to CASA image mask into which interactive masks will be added.
144
+ None can be used to indicate that the mask should be created
145
+ (if ``create=True``) and the name should be based upon the image name
146
+
147
+ create: bool
148
+ if True then the mask will be created if it does not exist
149
+ if False a mask path which does not exist results in an exception
150
+ '''
151
+
152
+ ###
153
+ ### Create application context (which includes a temporary directory).
154
+ ### This sets the title of the plot.
155
+ ###
156
+ self._app_state = AppContext( 'Create Mask' )
157
+
158
+ ###
159
+ ### widgets shared across image tabs (masking multiple images)
160
+ ###
161
+ self._cube_palette = None
162
+ self._image_bitmask_controls = None
163
+
164
+ ###
165
+ ### With Bokeh 3.2.2, the spectrum and convergence plots extend beyond the edge of the
166
+ ### browser window (requiring scrolling) if a width is not specified. It could be that
167
+ ### this should be computed from the width of the tabbed control area at the right of
168
+ ### the image display.
169
+ ###
170
+ self._spect_plot_width = 450
171
+
172
+ ###
173
+ ### Validate image paths
174
+ ###
175
+ if isinstance(image, str):
176
+ image_paths = [ image ]
177
+ elif isinstance(image, list) and all(isinstance(x, str) for x in image):
178
+ image_paths = image
179
+ else:
180
+ raise RuntimeError( 'CreateMask: image parameter should be a string or a list of strings' )
181
+
182
+ if len(image_paths) == 0:
183
+ raise RuntimeError( 'CreateMask: at least one image path must be specified' )
184
+
185
+ for img in image_paths:
186
+ if not exists(img):
187
+ raise RuntimeError(f'''CreateMask: image path '{img}' does not exist''')
188
+
189
+ ###
190
+ ### Validate mask paths
191
+ ###
192
+ mask_paths = None
193
+ if mask is None:
194
+ mask_paths = [None] * len(image_paths)
195
+ elif isinstance(mask,str):
196
+ if len(image_paths) != 1:
197
+ raise RuntimeError( 'CreateMask: when specifying masks, one must be provided for each image (even if its value is None)' )
198
+ mask_paths = [mask]
199
+ elif isinstance(mask,list):
200
+ if len(mask) == 0:
201
+ mask_paths = [None] * len(image_paths)
202
+ elif len(image_paths) != len(mask):
203
+ raise RuntimeError( 'CreateMask: when specifying masks, one must be provided for each image (even if its value is None)' )
204
+ elif all(isinstance(x, str) or x is None for x in mask):
205
+ mask_paths = mask
206
+ else:
207
+ raise RuntimeError( 'CreateMask: image parameter should be a string or a list of elements whose type is string or None' )
208
+
209
+ ###
210
+ ### Create mask paths if necessary
211
+ ###
212
+ if not create:
213
+ self._paths = self.__expand_mask_paths( zip(image_paths, mask_paths) )
214
+ for _,msk in self._paths:
215
+ if not exists(msk):
216
+ raise RuntimeError(f'''CreateMask: mask path '{msk}' does not exist (and create=False)''')
217
+ else:
218
+ self._paths = self.__create_masks( self.__expand_mask_paths( zip(image_paths, mask_paths) ) )
219
+
220
+ if not all((casaimage.shape(p[0]) == casaimage.shape(p[1])).all( ) for p in self._paths):
221
+ raise RuntimeError('CreateMask: mismatch between image and mask shapes')
222
+
223
+
224
+ self._fig = { 'help': None, 'status': None }
225
+ self._mask_state = { }
226
+ self._ctrl_state = { }
227
+ initialization_registered = False
228
+ for paths in self._paths:
229
+ _,name = splitpath(paths[0])
230
+ imdetails = self._mask_state[name] = { 'gui': { 'image': {}, 'fig': {},
231
+ 'image-adjust': { } } }
232
+
233
+ ###
234
+ ### Use CubeMask init_script to set up a 'beforeunload' handler which will signal to
235
+ ### CubeMask that the app is shuting down. It should then send the final results to
236
+ ### Python and then call the provided callback (the Promise resolve function).
237
+ ###
238
+ ### Waiting for this to complete before the browser tab is unloaded requires waiting
239
+ ### for the promise to be resolved within an async function. Creating one to do the
240
+ ### await works. The promise is resolved when the 'done' function call the Promise's
241
+ ### resolve function which is provide to 'done' as its callback function.
242
+ ###
243
+ ### If debugging this, make only a small change before confirming that exit from the
244
+ ### Python asyncio loop continues to work... seems to be fiddly
245
+ ###
246
+ imdetails['gui']['cube'] = CubeMask( paths[0], mask=paths[1],
247
+ init_script = None if initialization_registered else \
248
+ CustomJS( args={ }, code='''
249
+ window.addEventListener( 'beforeunload',
250
+ (event) => {
251
+ function donePromise( ) {
252
+
253
+ return new Promise( (resolve,reject) => {
254
+ // call by name does not work here:
255
+ // document._cube_done(cb=resolve) ???
256
+ if ( document._cube_done ) document._cube_done(null,resolve)
257
+ } )
258
+ }
259
+ ( async () => { await donePromise( ) } )( )
260
+ } )''' ) )
261
+
262
+ initialization_registered = True
263
+ imdetails['image-channels'] = imdetails['gui']['cube'].shape( )[3]
264
+
265
+
266
+ imdetails['gui']['image']['src'] = imdetails['gui']['cube'].js_obj( )
267
+ imdetails['gui']['image']['fig'] = imdetails['gui']['cube'].image( grid=False, height_policy='max', width_policy='max' )
268
+
269
+ if self._fig['help'] is None:
270
+ self._fig['help'] = self._ctrl_state['help'] = imdetails['gui']['cube'].help( rows=[
271
+ '<tr><td><i>stop button</i></td><td>clicking the stop button will close the dialog and control to python</td></tr>' ],
272
+ position='right' )
273
+ imdetails['gui']['channel-ctrl'] = imdetails['gui']['cube'].channel_ctrl( )
274
+ imdetails['gui']['cursor-pixel-text'] = imdetails['gui']['cube'].pixel_tracking_text( margin=(-3, 5, 3, 30) )
275
+
276
+ self._fig['status'] = imdetails['gui']['status'] = imdetails['gui']['cube'].status_text( "<p>initialization</p>" , width=230, reuse=self._fig['status'] )
277
+ self._image_bitmask_controls = imdetails['gui']['cube'].bitmask_ctrl( reuse=self._image_bitmask_controls, button_type='light' )
278
+
279
+ ###
280
+ ### spectrum plot must be disabled during iteration due to "tap to change channel" functionality
281
+ ###
282
+ if imdetails['image-channels'] > 1:
283
+ imdetails['gui']['spectrum'] = imdetails['gui']['cube'].spectrum( orient='vertical', sizing_mode='stretch_height', width=self._spect_plot_width )
284
+ imdetails['gui']['slider'] = imdetails['gui']['cube'].slider( show_value=False, title='', margin=(14,5,5,5), sizing_mode="scale_width" )
285
+ imdetails['gui']['goto'] = imdetails['gui']['cube'].goto( )
286
+ else:
287
+ imdetails['gui']['spectrum'] = None
288
+ imdetails['gui']['slider'] = None
289
+ imdetails['gui']['goto'] = None
290
+
291
+ ###
292
+ ### This is used to tell whether the websockets have been initialized, but also to
293
+ ### indicate if __call__ is being called multiple times to allow for resetting Bokeh
294
+ ###
295
+ self.__initialized = False
296
+
297
+ ###
298
+ ### the asyncio future that is used to transmit the result from mask creation
299
+ ###
300
+ self.__result_future = None
301
+
302
+ def _create_colormap_adjust( self, imdetails ):
303
+ palette = imdetails['gui']['cube'].palette( reuse=self._cube_palette )
304
+ return column( row( Div(text="<div><b>Colormap:</b></div>",margin=(5,2,5,25)), palette ),
305
+ imdetails['gui']['cube'].colormap_adjust( ), sizing_mode='stretch_both' )
306
+
307
+
308
+ def _create_control_image_tab( self, imid, imdetails ):
309
+ result = Tabs( tabs= ( [ TabPanel( child=imdetails['gui']['spectrum'],
310
+ title='Spectrum' ) ] if imdetails['image-channels'] > 1 else [ ] ) +
311
+ [ TabPanel( child=self._create_colormap_adjust(imdetails),
312
+ title='Colormap' ),
313
+ TabPanel( child=imdetails['gui']['cube'].statistics( ),
314
+ title='Statistics' ) ],
315
+ width=500, sizing_mode='stretch_height', tabs_location='below' )
316
+
317
+ if not hasattr(self,'_image_control_tab_groups'):
318
+ self._image_control_tab_groups = { }
319
+
320
+ self._image_control_tab_groups[imid] = result
321
+ result.js_on_change( 'active', CustomJS( args={ },
322
+ code='''document._casa_last_control_tab = cb_obj.active''' ) )
323
+ return result
324
+
325
+ def _create_image_panel( self, imagetuple ):
326
+ imid, imdetails = imagetuple
327
+
328
+ return TabPanel( child=column( row( *imdetails['gui']['channel-ctrl'], imdetails['gui']['cube'].coord_ctrl( ),
329
+ *self._image_bitmask_controls,
330
+ #Spacer( height=5, height_policy="fixed", sizing_mode="scale_width" ),
331
+ imdetails['gui']['cursor-pixel-text'],
332
+ row( Spacer( sizing_mode='stretch_width' ),
333
+ imdetails['gui']['cube'].tapedeck( size='20px' ) if imdetails['image-channels'] > 1 else Div( ),
334
+ Spacer( height=5, width=350 ), width_policy='max' ),
335
+ width_policy='max' ),
336
+ row( imdetails['gui']['image']['fig'],
337
+ column( row( imdetails['gui']['goto'],
338
+ imdetails['gui']['slider'],
339
+ width_policy='max' ) if imdetails['image-channels'] > 1 else Div( ),
340
+ self._create_control_image_tab(imid, imdetails), height_policy='max' ),
341
+ height_policy='max', width_policy='max' ),
342
+ height_policy='max', width_policy='max' ), title=imid )
343
+
344
+ def _launch_gui( self ):
345
+ '''create and show GUI
346
+ '''
347
+ self.__initialized = True
348
+
349
+ cwidth = 64
350
+ cheight = 40
351
+
352
+ self._ctrl_state['stop'] = TipButton( button_type="danger", max_width=cwidth, max_height=cheight, name='stop',
353
+ icon=svg_icon(icon_name="iclean-stop", size=18),
354
+ tooltip=Tooltip( content=HTML( '''Clicking this button will cause this tab to close and control will return to Python.''' ),
355
+ position='left' ) )
356
+
357
+ self._ctrl_state['stop'].js_on_click( CustomJS( args={ },
358
+ code='''if ( confirm( "Are you sure you want to end this mask creation session and close the GUI?" ) ) {
359
+ if ( document._cube_done ) document._cube_done( )
360
+ }''' ) )
361
+
362
+
363
+
364
+ tab_panels = list( map( self._create_image_panel, self._mask_state.items( ) ) )
365
+
366
+ for imid, imdetails in self._mask_state.items( ):
367
+ imdetails['gui']['cube'].connect( )
368
+
369
+ image_tabs = Tabs( tabs=tab_panels, tabs_location='below', height_policy='max', width_policy='max' )
370
+
371
+ self._fig['layout'] = column(
372
+ row( self._fig['help'],
373
+ Spacer( height=self._ctrl_state['stop'].height, sizing_mode="scale_width" ),
374
+ Div( text="<div><b>status:</b></div>" ),
375
+ self._fig['status'],
376
+ self._ctrl_state['stop'], sizing_mode="scale_width" ),
377
+ row( image_tabs, height_policy='max', width_policy='max' ),
378
+ height_policy='max', width_policy='max' )
379
+
380
+ ###
381
+ ### Keep track of which image is currently active in document._casa_image_name (which is
382
+ ### initialized in self._js['initialize']). Also, update the current control sub-tab
383
+ ### when the field main-tab is changed. An attempt to manage this all within the
384
+ ### control sub-tabs using a reference to self._image_control_tab_groups from
385
+ ### each control sub-tab failed with:
386
+ ###
387
+ ### bokeh.core.serialization.SerializationError: circular reference
388
+ ###
389
+ image_tabs.js_on_change( 'active', CustomJS( args=dict( names=[ t[0] for t in self._mask_state.items( ) ],
390
+ itergroups=self._image_control_tab_groups ),
391
+ code='''if ( ! hasprop(document,'_casa_last_control_tab') ) {
392
+ document._casa_last_control_tab = 0
393
+ }
394
+ document._casa_image_name = names[cb_obj.active]
395
+ itergroups[document._casa_image_name].active = document._casa_last_control_tab''' ) )
396
+
397
+ # Change display type depending on runtime environment
398
+ if is_notebook( ):
399
+ output_notebook()
400
+ else:
401
+ ### Directory is created when an HTTP server is running
402
+ ### (MAX)
403
+ ### output_file(self._imagename+'_webpage/index.html')
404
+ pass
405
+
406
+ show(self._fig['layout'])
407
+
408
+ def _asyncio_loop( self ):
409
+ '''return the event loop which can be mixed in with an existing event loop
410
+ to allow GUI and websocket events to be processed.
411
+ '''
412
+ return self._cube.loop( )
413
+
414
+ def __call__( self ):
415
+ '''Display GUI using the event loop specified by ``loop``.
416
+ '''
417
+ async def _run_( ):
418
+ async with self.serve( ) as s:
419
+ await s
420
+ asyncio.run(_run_( ))
421
+ return self.result( )
422
+
423
+ @asynccontextmanager
424
+ async def serve( self ):
425
+ '''This function is intended for developers who would like to embed interactive
426
+ clean as a part of a larger GUI. This embedded use of interactive clean is not
427
+ currently supported and would require the addition of parameters to this function
428
+ as well as changes to the interactive clean implementation. However, this function
429
+ does expose the ``asyncio.Future`` that is used to signal completion of the
430
+ interactive cleaning operation, and it provides the coroutines which must be
431
+ managed by asyncio to make the interactive clean GUI responsive.
432
+ '''
433
+ self.__reset( )
434
+ self._launch_gui( )
435
+
436
+ async with CMC( *( [ ctx for img in self._mask_state.keys( ) for ctx in
437
+ [
438
+ self._mask_state[img]['gui']['cube'].serve(self.__stop),
439
+ ]
440
+ ] ) ):
441
+ self.__result_future = asyncio.Future( )
442
+ yield self.__result_future
443
+ ###async with self._cube.serve( self.__stop ) as cube:
444
+ ### self.__result_future = asyncio.Future( )
445
+ ### yield ( self.__result_future, { 'cube': cube } )
446
+
447
+ def __retrieve_result( self ):
448
+ '''If InteractiveClean had a return value, it would be filled in as part of the
449
+ GUI dialog between Python and JavaScript and this function would return it'''
450
+ if isinstance(self._error_result,Exception):
451
+ raise self._error_result
452
+ if self._error_result is not None:
453
+ return self._error_result
454
+ return self._paths
455
+
456
+ def result( self ):
457
+ '''If InteractiveClean had a return value, it would be filled in as part of the
458
+ GUI dialog between Python and JavaScript and this function would return it'''
459
+ if self.__result_future is None:
460
+ raise RuntimeError( 'no interactive clean result is available' )
461
+ return self.__result_future.result( )