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,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
|