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,513 @@
1
+ ########################################################################
2
+ #
3
+ # Copyright (C) 2024,2025
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 ``CreateRegion`` application for interactive creation
29
+ of astropy regions'''
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, grid
36
+ from bokeh.plotting import show
37
+ from bokeh.models import Button, CustomJS, TabPanel, Tabs, Spacer, Div, Dropdown
38
+ from cubevis.toolbox import CubeMask, AppContext, RegionList
39
+ from cubevis.bokeh.utils import svg_icon
40
+ from bokeh.io import curdoc
41
+ from bokeh.io import reset_output as reset_bokeh_output
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 CreateRegion:
50
+ '''Class that can be used to launch a createregion GUI with ``CreateRegion('test.im')( )``.
51
+ ``CreateRegion`` is implemented with the same libraries that are used to implement
52
+ ``InteractiveClean`` and ``CreateMask``. Regions drawn on the displalyed CASA image
53
+ and then returned as astropy regions.
54
+ '''
55
+
56
+ def __stop( self, result ):
57
+ self._drawn_regions = result
58
+ if not self.__result_future.done( ):
59
+ self.__result_future.set_result(self._drawn_regions)
60
+
61
+ def _abort_handler( self, err ):
62
+ self._error_result = err
63
+ self.__stop( )
64
+
65
+ def __reset( self ):
66
+ if self.__initialized:
67
+ reset_bokeh_output( )
68
+ reset_resource_manager( )
69
+
70
+ ###
71
+ ### reset asyncio result future
72
+ ###
73
+ self.__result_future = None
74
+
75
+ ###
76
+ ### used by data pipe (websocket) initialization function
77
+ ###
78
+ self.__initialized = False
79
+
80
+ self._image = None
81
+ ###
82
+ ### error or exception result
83
+ ###
84
+ self._error_result = None
85
+
86
+ def __expand_mask_paths( self, path_pairs ):
87
+ '''return expanded mask paths
88
+
89
+ Parameters
90
+ ----------
91
+ path_pairs: list of tuples
92
+ each tuple contains a string in the first element that represents an image path
93
+ and the second element is either a None which signals that the an mask path should
94
+ be generated based upon the image path or a string which represents that mask path
95
+ for the first element image.
96
+
97
+ Returns
98
+ -------
99
+ [ (str,str) ]
100
+ A list of tuples is returned. The first element of each
101
+ tuple is a string that represents the image path and the second element is a string
102
+ which represents the mask path.
103
+ '''
104
+ uniquified_names = { } ### dict to keep track of repeated iamges
105
+
106
+ def create_mask_path( impath ):
107
+ if impath in uniquified_names and uniquified_names[impath][0] > 1:
108
+ uniq = "%032d" % uniquified_names[impath][1]
109
+ uniquified_names[impath][1] += 1
110
+ else:
111
+ uniq = ''
112
+ path,file = splitpath(impath)
113
+ if len(path) > 0 and not exists(path):
114
+ raise RuntimeError( f'''CreateRegion: mask path '{path}' does not exist''' )
115
+ basename,ext = splitext(file)
116
+ return join( path, f'''{basename}{uniq}.mask''')
117
+
118
+ ### python zip etc can only be read once
119
+ path_pairs_ = list(path_pairs)
120
+ for p in path_pairs_:
121
+ if p[0] in uniquified_names:
122
+ uniquified_names[p[0]][0] += 1
123
+ else:
124
+ uniquified_names[p[0]] = [1,1]
125
+
126
+ return list( map( lambda p: p if p[1] is not None else (p[0],create_mask_path(p[0])), path_pairs_ ) )
127
+
128
+ def __create_masks( self, paths ):
129
+ '''Create missing masks...'''
130
+ ### Create any mask images which do not exist (if create=True)
131
+ return list( map( lambda p: (p[0],casaimage.new(*p)) if not exists(p[1]) else p, paths ) )
132
+
133
+
134
+ def __init__( self, image ):
135
+ '''create a ``createregion`` object which will display image planes from a CASA
136
+ image and allow the user to draw masks for each channel.
137
+
138
+ Parameters
139
+ ----------
140
+ image: str or list of str
141
+ path(s) to CASA image for which interactive regions will be drawn
142
+ '''
143
+
144
+ ###
145
+ ### Create application context (which includes a temporary directory).
146
+ ### This sets the title of the plot.
147
+ ###
148
+ self._app_state = AppContext( 'Create Region' )
149
+
150
+ ###
151
+ ### widgets shared across image tabs (masking multiple images)
152
+ ###
153
+ self._cube_palette = None
154
+ self._image_region_controls = None
155
+
156
+ ###
157
+ ### With Bokeh 3.2.2, the spectrum and convergence plots extend beyond the edge of the
158
+ ### browser window (requiring scrolling) if a width is not specified. It could be that
159
+ ### this should be computed from the width of the tabbed control area at the right of
160
+ ### the image display.
161
+ ###
162
+ self._spect_plot_width = 450
163
+
164
+ ###
165
+ ### Validate image paths
166
+ ###
167
+ if isinstance(image, str):
168
+ image_paths = [ image ]
169
+ elif isinstance(image, list) and all(isinstance(x, str) for x in image):
170
+ image_paths = image
171
+ else:
172
+ raise RuntimeError( 'CreateRegion: image parameter should be a string or a list of strings' )
173
+
174
+ if len(image_paths) == 0:
175
+ raise RuntimeError( 'CreateRegion: at least one image path must be specified' )
176
+
177
+ for img in image_paths:
178
+ if not exists(img):
179
+ raise RuntimeError(f'''CreateRegion: image path '{img}' does not exist''')
180
+
181
+ self._drawn_regions = { k: [] for k in image_paths }
182
+ self._fig = { 'help': None, 'status': None }
183
+ self._region_state = { }
184
+ self._ctrl_state = { }
185
+ initialization_registered = False
186
+ for path in image_paths:
187
+ _,name = splitpath(path)
188
+ imdetails = self._region_state[name] = { 'gui': { 'image': {}, 'fig': {},
189
+ 'image-adjust': { } } }
190
+
191
+ ###
192
+ ### Use CubeMask init_script to set up a 'beforeunload' handler which will signal to
193
+ ### CubeMask that the app is shuting down (with 'document._cube_done( )'). It should then
194
+ ### send the final results to Python and then call the provided callback (the Promise
195
+ ### resolve function).
196
+ ###
197
+ ### Waiting for this to complete before the browser tab is unloaded requires waiting
198
+ ### for the promise to be resolved within an async function. Creating one to do the
199
+ ### await works. The promise is resolved when the 'done' function call the Promise's
200
+ ### resolve function which is provide to 'done' as its callback function.
201
+ ###
202
+ ### If debugging this, make only a small change before confirming that exit from the
203
+ ### Python asyncio loop continues to work... seems to be fiddly
204
+ ###
205
+ imdetails['gui']['cube'] = CubeMask( path )
206
+
207
+ initialization_registered = True
208
+ imdetails['image-channels'] = imdetails['gui']['cube'].shape( )[3]
209
+
210
+
211
+ imdetails['gui']['image']['src'] = imdetails['gui']['cube'].js_obj( )
212
+ imdetails['gui']['image']['fig'] = imdetails['gui']['cube'].image( grid=False, height_policy='max', width_policy='max', maxanno=5 )
213
+
214
+ if self._fig['help'] is None:
215
+ self._fig['help'] = self._ctrl_state['help'] = imdetails['gui']['cube'].help( rows=[ '<tr><td><i>stop button</i></td><td>clicking the stop button will close the dialog and control to python</td></tr>' ],
216
+ position='right' )
217
+ imdetails['gui']['channel-ctrl'] = imdetails['gui']['cube'].channel_ctrl( )
218
+ imdetails['gui']['cursor-pixel-text'] = imdetails['gui']['cube'].pixel_tracking_text( margin=(-3, 5, 3, 30) )
219
+
220
+ self._fig['status'] = imdetails['gui']['status'] = imdetails['gui']['cube'].status_text( "<p>initialization</p>" , width=230, reuse=self._fig['status'] )
221
+
222
+ ###
223
+ ### spectrum plot must be disabled during iteration due to "tap to change channel" functionality
224
+ ###
225
+ if imdetails['image-channels'] > 1:
226
+ imdetails['gui']['spectrum'] = imdetails['gui']['cube'].spectrum( orient='vertical', sizing_mode='stretch_height', width=self._spect_plot_width )
227
+ imdetails['gui']['slider'] = imdetails['gui']['cube'].slider( show_value=False, title='', margin=(14,5,5,5), sizing_mode="scale_width" )
228
+ imdetails['gui']['goto'] = imdetails['gui']['cube'].goto( )
229
+ else:
230
+ imdetails['gui']['spectrum'] = None
231
+ imdetails['gui']['slider'] = None
232
+ imdetails['gui']['goto'] = None
233
+
234
+ init_args = { 'sources': { } }
235
+ last_cube = None
236
+ for k,v in self._region_state.items( ):
237
+ init_args['sources'][k] = v['gui']['image']['src']
238
+ last_cube = v['gui']['cube']
239
+
240
+ def create_close_code( callback='' ):
241
+ CUBE_CALLBACK = f''', {callback}''' if callback else ''
242
+ return f'''let source = null
243
+ const result = {{ }}
244
+ const srcmap = casalib.reduce( ( acc, img, src ) => {{
245
+ result[img] = {{ }}
246
+ acc[src.id] = img
247
+ if ( source == null ) source = src
248
+ return acc }}, sources, {{ }} )
249
+ if ( source && document._cube_done )
250
+ document._cube_done(
251
+ casalib.reduce(
252
+ (acc, poly) => {{
253
+ acc[srcmap[poly.source.id]][poly.label] = {{
254
+ channels: poly.getchans( ),
255
+ geometry: poly.geometry,
256
+ styling: poly.styling }}
257
+ return acc }},
258
+ source._polys.list( ),
259
+ result )
260
+ {CUBE_CALLBACK} )'''
261
+
262
+ ##
263
+ ## Previously this beforeunload setup was accomplished by adding this to one of the
264
+ ## CubeMask.init_script. This worked until a reference was needed to all of the
265
+ ## ImageDataSources (which is an element of CubeMask). This resulted in a circular
266
+ ## reference when the plot was being rendered.
267
+ ##
268
+ ## The document._cube_done callback ( `(msg) => { resolve(true); return false }` ) can
269
+ ## return `false` (indicating that the window should not be closed by _cube_done)
270
+ ## because the window is already being closed when this beforeunload callback
271
+ ## is called.
272
+ ##
273
+ curdoc( ).js_on_event( 'document_ready', CustomJS( args=init_args,
274
+ code=f'''window.addEventListener( 'beforeunload',
275
+ (event) => {{
276
+ function donePromise( ) {{
277
+ return new Promise(
278
+ (resolve,reject) => {{
279
+ {create_close_code('(msg) => { resolve(true); return false }')}
280
+ }} )
281
+ }}
282
+ ( async () => {{ await donePromise( ) }} )( )
283
+ }} )''' ) )
284
+
285
+ self._ctrl_state['stop'] = TipButton( button_type="danger", max_width=64, max_height=40, name='stop',
286
+ icon=svg_icon(icon_name="iclean-stop", size=18),
287
+ tooltip=Tooltip( content=HTML( '''Clicking this button will cause this tab to close and control will return to Python.''' ),
288
+ position='left' ) )
289
+
290
+ self._ctrl_state['stop'].js_on_click( CustomJS( args=init_args,
291
+ code=f'''if ( confirm( "Are you sure you want to end this mask creation session and close the GUI?" ) ) {{
292
+ {create_close_code( )}
293
+ }}''' ) )
294
+
295
+ ###
296
+ ### This is used to tell whether the websockets have been initialized, but also to
297
+ ### indicate if __call__ is being called multiple times to allow for resetting Bokeh
298
+ ###
299
+ self.__initialized = False
300
+
301
+ ###
302
+ ### the asyncio future that is used to transmit the result from region creation
303
+ ###
304
+ self.__result_future = None
305
+
306
+ def _create_style_adjust( self, imdetails ):
307
+
308
+ if 'region-styling' not in imdetails['gui']:
309
+ ### also used in self._create_location_panel
310
+ self._image_region_controls = imdetails['gui']['region-styling'] = imdetails['gui']['cube'].region_style_ctrl( reuse=self._image_region_controls, button_type='light' )
311
+
312
+ hover = row( column( Div(text='<div>Fill</div>'),
313
+ row(*imdetails['gui']['region-styling']['hover']['fill']),
314
+ Div(text='<div>Line</div>'),
315
+ row(*imdetails['gui']['region-styling']['hover']['line']) ) )
316
+ hover.styles = {"border": "1px solid black", "padding": "1px" }
317
+ default = row( column( Div(text='<div>Fill</div>'),
318
+ row(*imdetails['gui']['region-styling']['default']['fill']),
319
+ Div(text='<div>Line</div>'),
320
+ row(*imdetails['gui']['region-styling']['default']['line']) ) )
321
+ default.styles = { "border": "1px solid black", "padding": "1px" }
322
+ return column( Div(text='<div><b>Styling for Region with Focus</b></div>'),
323
+ hover,
324
+ Div(text='<div><b>Default Style for New Regions</b></div>'),
325
+ default )
326
+
327
+ def _create_location_panel( self, imdetails, width=410 ):
328
+ pos = imdetails['gui']['cube'].region_position_ctrl( )
329
+ coords = grid( [ [ None, Div(text='<b>X</b>'), Div(text='<b>Y</b>') ],
330
+ [ Div(text='<b>pixel</b>'), *pos['pixel'] ],
331
+ [ Div(text='<b>world</b>'), *pos['world'] ] ] )
332
+ coord_section = row( column( Div(text='<b>Region Placement</b>'),
333
+ coords ),
334
+ width=width )
335
+ chan_section = row( column( Div(text='<b>Channels</b>'),
336
+ *[ row( Div(text=f'''<b>{s}</b>'''), text, sizing_mode='stretch_width' ) for s,text in pos['chan'].items( ) ],
337
+ sizing_mode='stretch_width' ),
338
+ width=width )
339
+
340
+ coord_section.styles = {"border": "1px solid black", "padding": "1px" }
341
+ chan_section.styles = {"border": "1px solid black", "padding": "1px" }
342
+
343
+ tracking = column( Div(text='<b>Tracking</b>', styles={"margin-top": "10px"} ), row( *pos['tracking'] ), sizing_mode='stretch_width' )
344
+ tracking.styles = {"border": "1px solid black", "padding": "1px" }
345
+
346
+ if 'region-styling' not in imdetails['gui']:
347
+ ### also used in self._create_style_adjust
348
+ self._image_region_controls = imdetails['gui']['region-styling'] = imdetails['gui']['cube'].region_style_ctrl( reuse=self._image_region_controls, button_type='light' )
349
+
350
+ style = row( column( Div(text='<div>Fill</div>'),
351
+ row(*imdetails['gui']['region-styling']['selected']['fill']),
352
+ Div(text='<div>Line</div>'),
353
+ row(*imdetails['gui']['region-styling']['selected']['line']) ) )
354
+ style.styles = {"border": "1px solid black", "padding": "1px" }
355
+
356
+ return column( coord_section,
357
+ chan_section,
358
+ row( pos['status'] ),
359
+ row( column( pos['label'],
360
+ tracking, width=140 ),
361
+ Spacer(width=4),
362
+ style ) )
363
+
364
+ def _create_colormap_adjust( self, imdetails ):
365
+ palette = imdetails['gui']['cube'].palette( reuse=self._cube_palette )
366
+ return column( row( Div(text="<div><b>Colormap:</b></div>",margin=(5,2,5,25)), palette ),
367
+ imdetails['gui']['cube'].colormap_adjust( ), sizing_mode='stretch_both' )
368
+
369
+
370
+ def _create_control_image_tab( self, imid, imdetails ):
371
+ result = Tabs( tabs= [ TabPanel( child=self._create_location_panel(imdetails),
372
+ title='Placement' ),
373
+ TabPanel( child=self._create_style_adjust(imdetails),
374
+ title='Config' ) ] +
375
+ ( [ TabPanel( child=imdetails['gui']['spectrum'],
376
+ title='Spectrum' ) ] if imdetails['image-channels'] > 1 else [ ] ) +
377
+ [ TabPanel( child=self._create_colormap_adjust(imdetails),
378
+ title='Colormap' ),
379
+ TabPanel( child=imdetails['gui']['cube'].statistics( ),
380
+ title='Statistics' ) ],
381
+ width=500, sizing_mode='stretch_height', tabs_location='below' )
382
+
383
+ if not hasattr(self,'_image_control_tab_groups'):
384
+ self._image_control_tab_groups = { }
385
+
386
+ self._image_control_tab_groups[imid] = result
387
+ result.js_on_change( 'active', CustomJS( args=dict( ),
388
+ code='''document._casa_last_control_tab = cb_obj.active''' ) )
389
+ return result
390
+
391
+ def _create_image_panel( self, imagetuple ):
392
+ imid, imdetails = imagetuple
393
+
394
+ return TabPanel( child=column( row( *imdetails['gui']['channel-ctrl'], imdetails['gui']['cube'].coord_ctrl( ),
395
+ ##Spacer( height=5, height_policy="fixed", sizing_mode="scale_width" ),
396
+ imdetails['gui']['cursor-pixel-text'],
397
+ row( Spacer( sizing_mode='stretch_width' ),
398
+ imdetails['gui']['cube'].tapedeck( size='20px' ) if imdetails['image-channels'] > 1 else Div( ),
399
+ Spacer( height=5, width=350 ), width_policy='max' ),
400
+ width_policy='max' ),
401
+ row( imdetails['gui']['image']['fig'],
402
+ column( row( imdetails['gui']['goto'],
403
+ imdetails['gui']['slider'],
404
+ width_policy='max' ) if imdetails['image-channels'] > 1 else Div( ),
405
+ self._create_control_image_tab(imid, imdetails), height_policy='max' ),
406
+ height_policy='max', width_policy='max' ),
407
+ height_policy='max', width_policy='max' ), title=imid )
408
+
409
+ def _launch_gui( self ):
410
+ '''create and show GUI
411
+ '''
412
+ self.__initialized = True
413
+
414
+ width = 35
415
+ height = 35
416
+
417
+ tab_panels = list( map( self._create_image_panel, self._region_state.items( ) ) )
418
+
419
+ for imid, imdetails in self._region_state.items( ):
420
+ imdetails['gui']['cube'].connect( )
421
+
422
+ image_tabs = Tabs( tabs=tab_panels, tabs_location='below', height_policy='max', width_policy='max' )
423
+
424
+ self._fig['layout'] = column(
425
+ row( self._fig['help'],
426
+ Spacer( height=self._ctrl_state['stop'].height, sizing_mode="scale_width" ),
427
+ Div( text="<div><b>status:</b></div>" ),
428
+ self._fig['status'],
429
+ self._ctrl_state['stop'], sizing_mode="scale_width" ),
430
+ row( image_tabs, height_policy='max', width_policy='max' ),
431
+ height_policy='max', width_policy='max' )
432
+
433
+ ###
434
+ ### Keep track of which image is currently active in document._casa_image_name (which is
435
+ ### initialized in self._js['initialize']). Also, update the current control sub-tab
436
+ ### when the field main-tab is changed. An attempt to manage this all within the
437
+ ### control sub-tabs using a reference to self._image_control_tab_groups from
438
+ ### each control sub-tab failed with:
439
+ ###
440
+ ### bokeh.core.serialization.SerializationError: circular reference
441
+ ###
442
+ image_tabs.js_on_change( 'active', CustomJS( args=dict( names=[ t[0] for t in self._region_state.items( ) ],
443
+ itergroups=self._image_control_tab_groups ),
444
+ code='''if ( ! hasprop(document,'_casa_last_control_tab') ) {
445
+ document._casa_last_control_tab = 0
446
+ }
447
+ document._casa_image_name = names[cb_obj.active]
448
+ itergroups[document._casa_image_name].active = document._casa_last_control_tab''' ) )
449
+
450
+ # Change display type depending on runtime environment
451
+ if is_notebook( ):
452
+ output_notebook()
453
+ else:
454
+ ### Directory is created when an HTTP server is running
455
+ ### (MAX)
456
+ ### output_file(self._imagename+'_webpage/index.html')
457
+ pass
458
+
459
+ show(self._fig['layout'])
460
+
461
+ def _asyncio_loop( self ):
462
+ '''return the event loop which can be mixed in with an existing event loop
463
+ to allow GUI and websocket events to be processed.
464
+ '''
465
+ return self._cube.loop( )
466
+
467
+ def __call__( self ):
468
+ '''Display GUI using the event loop specified by ``loop``.
469
+ '''
470
+ async def _run_( ):
471
+ async with self.serve( ) as s:
472
+ await s
473
+ asyncio.run(_run_( ))
474
+ return self.result( )
475
+
476
+ @asynccontextmanager
477
+ async def serve( self ):
478
+ '''This function is intended for developers who would like to embed interactive
479
+ clean as a part of a larger GUI. This embedded use of interactive clean is not
480
+ currently supported and would require the addition of parameters to this function
481
+ as well as changes to the interactive clean implementation. However, this function
482
+ does expose the ``asyncio.Future`` that is used to signal completion of the
483
+ interactive cleaning operation, and it provides the coroutines which must be
484
+ managed by asyncio to make the interactive clean GUI responsive.
485
+ '''
486
+ self.__reset( )
487
+ self._launch_gui( )
488
+
489
+ async with CMC( *( [ ctx for img in self._region_state.keys( ) for ctx in
490
+ [
491
+ self._region_state[img]['gui']['cube'].serve(self.__stop),
492
+ ]
493
+ ] ) ):
494
+ self.__result_future = asyncio.Future( )
495
+ yield self.__result_future
496
+ ###async with self._cube.serve( self.__stop ) as cube:
497
+ ### self.__result_future = asyncio.Future( )
498
+ ### yield ( self.__result_future, { 'cube': cube } )
499
+
500
+ def result( self ):
501
+ '''If InteractiveClean had a return value, it would be filled in as part of the
502
+ GUI dialog between Python and JavaScript and this function would return it'''
503
+ if self.__result_future is None:
504
+ raise RuntimeError( 'no interactive clean result is available' )
505
+ if not self.__result_future.done( ):
506
+ raise RuntimeError( 'regions not yet available to be returned' )
507
+
508
+ result = { }
509
+ for img, regions in self.__result_future.result( ).items( ):
510
+ _, fits_header = self._region_state[img]['gui']['cube'].fits_header( )
511
+ result[img] = RegionList( regions, fits_header )
512
+
513
+ return result