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.
- cubevis/LICENSE.rst +500 -0
- cubevis/__icons__/20px/fast-backward.svg +13 -0
- cubevis/__icons__/20px/fast-forward.svg +13 -0
- cubevis/__icons__/20px/step-backward.svg +12 -0
- cubevis/__icons__/20px/step-forward.svg +12 -0
- cubevis/__icons__/add-chan.png +0 -0
- cubevis/__icons__/add-chan.svg +84 -0
- cubevis/__icons__/add-cube.png +0 -0
- cubevis/__icons__/add-cube.svg +186 -0
- cubevis/__icons__/drag.png +0 -0
- cubevis/__icons__/drag.svg +109 -0
- cubevis/__icons__/mask-selected.png +0 -0
- cubevis/__icons__/mask.png +0 -0
- cubevis/__icons__/mask.svg +1 -0
- cubevis/__icons__/new-layer-sm-selected.png +0 -0
- cubevis/__icons__/new-layer-sm-selected.svg +88 -0
- cubevis/__icons__/new-layer-sm.png +0 -0
- cubevis/__icons__/new-layer-sm.svg +15 -0
- cubevis/__icons__/reset.png +0 -0
- cubevis/__icons__/reset.svg +11 -0
- cubevis/__icons__/sub-chan.png +0 -0
- cubevis/__icons__/sub-chan.svg +71 -0
- cubevis/__icons__/sub-cube.png +0 -0
- cubevis/__icons__/sub-cube.svg +95 -0
- cubevis/__icons__/zoom-to-fit.png +0 -0
- cubevis/__icons__/zoom-to-fit.svg +21 -0
- cubevis/__init__.py +58 -0
- cubevis/__js__/bokeh-3.6.1.min.js +728 -0
- cubevis/__js__/bokeh-tables-3.6.1.min.js +119 -0
- cubevis/__js__/bokeh-widgets-3.6.1.min.js +141 -0
- cubevis/__js__/casalib.min.js +1 -0
- cubevis/__js__/cubevisjs.min.js +62 -0
- cubevis/__version__.py +1 -0
- cubevis/apps/__init__.py +44 -0
- cubevis/apps/_createmask.py +461 -0
- cubevis/apps/_createregion.py +513 -0
- cubevis/apps/_interactiveclean.py +3260 -0
- cubevis/apps/_interactiveclean_wrappers.py +130 -0
- cubevis/apps/_ms_raster.py +815 -0
- cubevis/apps/_plotants.py +286 -0
- cubevis/apps/_plotbandpass.py +7 -0
- cubevis/bokeh/__init__.py +29 -0
- cubevis/bokeh/annotations/__init__.py +1 -0
- cubevis/bokeh/annotations/_ev_poly_annotation.py +6 -0
- cubevis/bokeh/components/__init__.py +28 -0
- cubevis/bokeh/format/__init__.py +31 -0
- cubevis/bokeh/format/_time_ticks.py +44 -0
- cubevis/bokeh/format/_wcs_ticks.py +45 -0
- cubevis/bokeh/models/__init__.py +4 -0
- cubevis/bokeh/models/_edit_span.py +7 -0
- cubevis/bokeh/models/_ev_text_input.py +6 -0
- cubevis/bokeh/models/_tip.py +37 -0
- cubevis/bokeh/models/_tip_button.py +50 -0
- cubevis/bokeh/sources/__init__.py +35 -0
- cubevis/bokeh/sources/_data_pipe.py +258 -0
- cubevis/bokeh/sources/_image_data_source.py +83 -0
- cubevis/bokeh/sources/_image_pipe.py +581 -0
- cubevis/bokeh/sources/_spectra_data_source.py +55 -0
- cubevis/bokeh/sources/_updatable_data_source.py +189 -0
- cubevis/bokeh/state/__init__.py +34 -0
- cubevis/bokeh/state/_initialize.py +164 -0
- cubevis/bokeh/state/_javascript.py +53 -0
- cubevis/bokeh/state/_palette.py +58 -0
- cubevis/bokeh/state/_session.py +44 -0
- cubevis/bokeh/state/js/bokeh-2.4.1.min.js +596 -0
- cubevis/bokeh/state/js/bokeh-gl-2.4.1.min.js +74 -0
- cubevis/bokeh/state/js/bokeh-tables-2.4.1.min.js +132 -0
- cubevis/bokeh/state/js/bokeh-widgets-2.4.1.min.js +118 -0
- cubevis/bokeh/state/js/casaguijs-v0.0.4.0-b2.4.min.js +49 -0
- cubevis/bokeh/state/js/casaguijs-v0.0.5.0-b2.4.min.js +49 -0
- cubevis/bokeh/state/js/casaguijs-v0.0.6.0-b2.4.min.js +49 -0
- cubevis/bokeh/state/js/casalib-v0.0.1.min.js +1 -0
- cubevis/bokeh/tools/__init__.py +31 -0
- cubevis/bokeh/tools/_cbreset_tool.py +52 -0
- cubevis/bokeh/tools/_drag_tool.py +61 -0
- cubevis/bokeh/utils/__init__.py +35 -0
- cubevis/bokeh/utils/_axes_labels.py +94 -0
- cubevis/bokeh/utils/_svg_icon.py +136 -0
- cubevis/data/__init__.py +1 -0
- cubevis/data/casaimage/__init__.py +114 -0
- cubevis/data/measurement_set/__init__.py +7 -0
- cubevis/data/measurement_set/_ms_data.py +178 -0
- cubevis/data/measurement_set/processing_set/__init__.py +30 -0
- cubevis/data/measurement_set/processing_set/_ps_concat.py +98 -0
- cubevis/data/measurement_set/processing_set/_ps_coords.py +78 -0
- cubevis/data/measurement_set/processing_set/_ps_data.py +213 -0
- cubevis/data/measurement_set/processing_set/_ps_io.py +55 -0
- cubevis/data/measurement_set/processing_set/_ps_raster_data.py +154 -0
- cubevis/data/measurement_set/processing_set/_ps_select.py +91 -0
- cubevis/data/measurement_set/processing_set/_ps_stats.py +218 -0
- cubevis/data/measurement_set/processing_set/_xds_data.py +149 -0
- cubevis/plot/__init__.py +1 -0
- cubevis/plot/ms_plot/__init__.py +29 -0
- cubevis/plot/ms_plot/_ms_plot.py +242 -0
- cubevis/plot/ms_plot/_ms_plot_constants.py +22 -0
- cubevis/plot/ms_plot/_ms_plot_selectors.py +348 -0
- cubevis/plot/ms_plot/_raster_plot.py +292 -0
- cubevis/plot/ms_plot/_raster_plot_inputs.py +116 -0
- cubevis/plot/ms_plot/_xds_plot_axes.py +110 -0
- cubevis/private/__java__/xml-casa-assembly-1.86.jar +0 -0
- cubevis/private/_gclean.py +798 -0
- cubevis/private/casashell/createmask.py +332 -0
- cubevis/private/casashell/iclean.py +4432 -0
- cubevis/private/casatasks/__init__.py +140 -0
- cubevis/private/casatasks/createmask.py +86 -0
- cubevis/private/casatasks/createregion.py +83 -0
- cubevis/private/casatasks/iclean.py +1831 -0
- cubevis/readme.rst +16 -0
- cubevis/remote/__init__.py +10 -0
- cubevis/remote/_gclean.py +61 -0
- cubevis/remote/_local.py +287 -0
- cubevis/remote/_remote_kernel.py +80 -0
- cubevis/toolbox/__init__.py +32 -0
- cubevis/toolbox/_app_context.py +74 -0
- cubevis/toolbox/_cube.py +3457 -0
- cubevis/toolbox/_region_list.py +197 -0
- cubevis/utils/_ResourceManager.py +86 -0
- cubevis/utils/__init__.py +620 -0
- cubevis/utils/_contextmgrchain.py +84 -0
- cubevis/utils/_conversion.py +93 -0
- cubevis/utils/_copydoc.py +55 -0
- cubevis/utils/_docenum.py +25 -0
- cubevis/utils/_import_protected_module.py +35 -0
- cubevis/utils/_logging.py +85 -0
- cubevis/utils/_pkgs.py +77 -0
- cubevis/utils/_regions.py +40 -0
- cubevis/utils/_static.py +66 -0
- cubevis/utils/_tiles.py +167 -0
- cubevis-0.5.2.dist-info/METADATA +151 -0
- cubevis-0.5.2.dist-info/RECORD +132 -0
- cubevis-0.5.2.dist-info/WHEEL +4 -0
- 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( )
|