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,3457 @@
1
+ #######################################################################
2
+ #
3
+ # Copyright (C) 2022,2023,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
+ '''This provides an implementation of ``CubeMask`` which allows interactive
29
+ clean and makemask to share a common implementaton. The user calls member
30
+ functions to create widgets which can be placed in the GUI created by the
31
+ calling application. Once all of the widgets have been created. The
32
+ ``connect`` member function creates all of the Bokeh/JavaScript callbacks
33
+ that allow the widgets to interact'''
34
+
35
+ import math
36
+ from os import path
37
+ import asyncio
38
+ from uuid import uuid4
39
+ from sys import platform
40
+ from os.path import dirname, join
41
+ import websockets
42
+ from contextlib import asynccontextmanager
43
+ from bokeh.core.enums import HatchPattern as _hatch_patterns
44
+ from bokeh.core.enums import DashPattern as _dash_patterns
45
+ from bokeh.events import SelectionGeometry, MouseEnter, MouseLeave, MouseMove, LODStart, LODEnd, ValueSubmit
46
+ from bokeh.models import PolyAnnotation
47
+ from bokeh.models import CustomJS, CustomAction, Slider, Div, Span, HoverTool, TableColumn, \
48
+ DataTable, Select, ColorPicker, Spinner, Select, Button, PreText, Dropdown, \
49
+ LinearColorMapper, TextInput, Spacer, InlineStyleSheet, Quad
50
+ from bokeh.models import WheelZoomTool, PanTool, ResetTool, PolySelectTool, LassoSelectTool, BoxSelectTool, SaveTool, ResetTool
51
+ from bokeh.models import BasicTickFormatter
52
+ from bokeh.plotting import ColumnDataSource, figure
53
+ from cubevis.bokeh.sources import ImageDataSource, ImagePipe, DataPipe
54
+ from cubevis.bokeh.format import WcsTicks
55
+ from cubevis.bokeh.models import EditSpan
56
+ from ..data import casaimage
57
+ from ..utils import pack_arrays, find_ws_address, set_attributes, resource_manager, polygon_indexes, is_notebook
58
+ from ..bokeh.models import EvTextInput
59
+ from ..bokeh.tools import CBResetTool
60
+ from ..bokeh.state import available_palettes, find_palette, default_palette
61
+ from ..bokeh.annotations import EvPolyAnnotation
62
+ from bokeh.layouts import row, column
63
+ from bokeh.models.dom import HTML
64
+ from bokeh.models import Tooltip
65
+ from ..bokeh.models import TipButton, Tip
66
+ from cubevis.bokeh.utils import svg_icon
67
+
68
+ import numpy as np
69
+
70
+ _hatches = [str(p) for p in _hatch_patterns]
71
+ _dashes = [str(p) for p in _dash_patterns]
72
+
73
+ class CubeMask:
74
+ '''Class which provides a common implementation of Bokeh widget behavior for
75
+ interactive clean and make mask'''
76
+
77
+ def __init__( self, image, mask=None, abort=None, init_script=None ):
78
+ '''Create a cube masking GUI which includes the 2-D raster cube plane display
79
+ along with these optional components:
80
+
81
+ * slider to move through planes
82
+ * spectrum plot (in response to mouse movements in 2-D raster display)
83
+ * statistics (table)
84
+
85
+ Parameters
86
+ ----------
87
+ image: str
88
+ path to CASA image for which interactive masks will be drawn
89
+ mask: str or None
90
+ If provided, this shifts the masking to bitmask operation. In
91
+ bitmask operation, the drawn regions are used to add or subtract
92
+ from a bitmask cube image instead of being the union of all drawn
93
+ regions. This is the standard mode of operation for interactive clean.
94
+ abort: function
95
+ If provided, the ``abort`` function will be called in the case of an error.
96
+ init_script: CustomJS script
97
+ Script to run upon initialization of Cube
98
+ '''
99
+ self.init_script = init_script
100
+ self.COUNT = 1
101
+ self.CCOUNT = 1
102
+
103
+ self._is_notebook = is_notebook()
104
+ #self._color = '#00FF00' # anti-green user feedback (issue #40 2024-05-02 13:08:32)
105
+ self._region_style=dict( fill_alpha=0, hover_fill_alpha=0.3,
106
+ fill_color='white', hover_fill_color='white',
107
+ line_color='white', hover_line_color='white',
108
+ line_dash='solid', hover_line_dash='solid',
109
+ line_width=1, hover_line_width=3,
110
+ line_alpha=1, hover_line_alpha=0.6,
111
+ hatch_pattern=_hatches[0], hover_hatch_pattern='blank' )
112
+
113
+ self._region_style_tips = dict( default_fill_color="Default fill color for newly created regions",
114
+ default_fill_alpha="Default fill alpha for newly created regions",
115
+ default_hatch_pattern="Default hatch fill pattern for newly created regions",
116
+ default_line_color="Default line color for newly created regions",
117
+ default_line_width="Default line width for newly created regions",
118
+ default_line_alpha="Default line alpha for newly created regions",
119
+ default_line_dash="Default line dash for newly created regions",
120
+
121
+ selected_fill_color="Fill color for the selected region",
122
+ selected_fill_alpha="Fill alpha for the selected region",
123
+ selected_hatch_pattern="Hatch fill pattern for the selected region",
124
+ selected_line_color="Line color for the selected region",
125
+ selected_line_width="Line width for the selected region",
126
+ selected_line_alpha="Line alpha for the selected region",
127
+ selected_line_dash="Line dash for the selected region",
128
+
129
+ hover_fill_color="Fill color for region with cursor focus",
130
+ hover_fill_alpha="Fill alpha for region with cursor focus",
131
+ hover_hatch_pattern="Hatch fill pattern for region with cursor focus",
132
+ hover_line_color="Line color for region with cursor focus",
133
+ hover_line_width="Line width for region with cursor focus",
134
+ hover_line_alpha="Line alpha for region with cursor focus",
135
+ hover_line_dash="Line dash for region with cursor focus" )
136
+
137
+ self._stop_serving_function = None # function supplied when starting serving
138
+ self._image_path = image # path to image cube to be displayed
139
+ self._mask_path = mask # path to bitmask cube (if any)
140
+ self._region_controls={'coord':{'initialized': False}, # ONLY USED WITH NO MASK CUBE
141
+ 'tracking': {} } # gui styling for regions
142
+ self._region_controls['tracking']['pointer'] = None # pointer (PolyAnnotation) that identifies the center point of a region
143
+ self._region_controls['tracking']['color'] = '#ff0000' # color for the tracking dot when dragging regions
144
+ self._region_controls['coord']['chan'] = { } # channel ranges for regions ONLY USED WITH NO MASK CUBE
145
+ self._region_controls_newpoly = None # hook to add a callback for creation of poly annotations
146
+ self._mask_id = None # id for each unique mask
147
+ self._image = None # figure displaying cube & mask planes
148
+ self._channel_ctrl = None # display channel and stokes
149
+ self._stokes_labels = None # stokes labels for the image cube
150
+ self._channel_ctrl_stokes_dropdown = None # drop down for changing stokes when _channel_ctrl is used
151
+ self._channel_ctrl_group = None # row for channel control group
152
+ self._coord_ctrl_dropdown = None # select pixel or world
153
+ self._coord_ctrl_group = None # row for coordinate control group
154
+ self._status_div = None # status line (used to report problems)
155
+ self._pixel_tracking_text = None # cursor tracking pixel value
156
+ self._chan_image = None # channel image
157
+ self._bitmask = None # bitmask image
158
+ self._bitmask_contour = None # bitmask MultiPolygon contour
159
+ self._bitmask_contour_ds = None # bitmask MultiPolygon contour data source
160
+ self._bitmask_color_selector = None # bitmask color selector
161
+ self._bitmask_transparency_button = None # select whether the 1s or 0s is transparent
162
+ self._bitmask_contour_maskmod = None # display mask contour as a region MultiPolygon (for copy/paste)
163
+ self._bitmask_contour_maskmod_ds = None # display mask contour data source
164
+ self._mask0 = None # INTERNAL systhesis imaging mask
165
+ self._goto = None # goto channel row (contains text input and dropdown)
166
+ self._goto_txt = None # goto channel text input
167
+ self._goto_stokes = None # goto channel stokes dropdown
168
+ self._slider = None # slider to move from plane to plane
169
+ self._tapedeck = None # buttons to move the slider
170
+ self._spectrum = None # figure displaying spectrum along the frequency axis
171
+ self._statistics = None # statistics data table
172
+ self._statistics_mask = None # button to switch from channel statistics to mask statistics
173
+ self._statistics_use_mask = False # whether statistics calculations will be based on the masked
174
+ # area or the whole channel
175
+ self._palette = None # palette selection
176
+ self._help_button = None # help button that creates a new tab/window (instead of hide/show Div)
177
+ self._image_spectrum = None # spectrum data source
178
+ self._image_source = None # ImageDataSource
179
+ self._statistics_source = None
180
+ self._pipe = { 'image': None, 'control': None } # data pipes
181
+ self._ids = { 'palette': str(uuid4( )),
182
+ 'mask-mod': str(uuid4( )),
183
+ 'done': str(uuid4( )),
184
+ 'config-statistics': str(uuid4( )),
185
+ 'fetch-spectrum': str(uuid4( )),
186
+ 'colormap-adjust': str(uuid4( )) } # ids used for control messages
187
+ self._hotkey_state = { } # used to disambiguate multiple CubeMasks in browser
188
+ self._image_freeze_cb = [ ] # CustomJS to be invoked when the use freezes cursor tracking (by typing 'f')
189
+ self._image_unfreeze_cb = [ ] # CustomJS to be invoked when cursor tracking is unfrozen
190
+
191
+ if self._mask_path:
192
+ ###########################################################################################################################
193
+ ### JavaScript init script to be run early in the startup. Piggybacked off of the ImagePipe initialization ###
194
+ ### CustomAction callbacks are set in connect( ) function. ###
195
+ ###########################################################################################################################
196
+ _add_ = dict( chan=casaimage.as_mime(join( dirname(dirname(__file__)), "__icons__", 'add-chan.png' ) ),
197
+ cube=casaimage.as_mime(join( dirname(dirname(__file__)), "__icons__", 'add-cube.png' ) ) )
198
+ _sub_ = dict( chan=casaimage.as_mime(join( dirname(dirname(__file__)), "__icons__", 'sub-chan.png' ) ),
199
+ cube=casaimage.as_mime(join( dirname(dirname(__file__)), "__icons__", 'sub-cube.png' ) ) )
200
+ self._mask_icons_ = dict( on=casaimage.as_mime(join( dirname(dirname(__file__)), "__icons__", 'new-layer-sm-selected.png' ) ),
201
+ off=casaimage.as_mime(join( dirname(dirname(__file__)), "__icons__", 'new-layer-sm.png' ) ) )
202
+ self._mask_add_sub = { 'add': CustomAction( icon=_add_['chan'], name="Mask Add",
203
+ description="add region to current channel's mask (hold Shift key then click to add to all channels)" ),
204
+ 'sub': CustomAction( icon=_sub_['chan'], name="Mask Sub",
205
+ description="subtract region from current channel's mask (hold Shift key then click to subtract from all channels)" ),
206
+ 'mask': CustomAction( icon=self._mask_icons_['off'], name="Mask Select",
207
+ description="select the mask for the current channel" ),
208
+ 'img': dict( add=_add_, sub=_sub_ ) }
209
+
210
+ self._fig = { }
211
+ self._hover = { 'spectrum': None, 'image': None } # HoverTools which are used to synchronize image/spectrum
212
+ # movement/taps and and corresponding display
213
+
214
+ self._result = None # result to be filled in from Bokeh
215
+
216
+ self._image_server = None
217
+ self._control_server = None
218
+
219
+ self._cb = { }
220
+ self._annotations = [ ] # statically allocate fixed poly annotations for (re)use
221
+ # on successive image cube planes
222
+
223
+ self._cm_adjust = { 'id': self._ids['colormap-adjust'],
224
+ 'bins': 256, # state for colormap_adjust(...)
225
+ 'min input': None,
226
+ 'max input': None,
227
+ 'span one': None,
228
+ 'span two': None,
229
+ 'histogram': None,
230
+ }
231
+
232
+ self.__abort = abort
233
+
234
+ if self.__abort is not None and not callable(self.__abort):
235
+ raise RuntimeError('abort function must be callable')
236
+
237
+ self.__init_js( )
238
+
239
+ def __stop( self ):
240
+ '''stop interactive masking
241
+ '''
242
+ if self._stop_serving_function:
243
+ self._stop_serving_function( self._result )
244
+
245
+ def _init_pipes( self ):
246
+ '''set up websockets
247
+ '''
248
+ self.__init_js( )
249
+ if self._pipe['image'] is None:
250
+ #######################################################################################################################
251
+ ### init_script code sets up Ctrl key handling for switching the add/subtract plot tool actions from single channel ###
252
+ ### operation to all channel operation ###
253
+ #######################################################################################################################
254
+ self._pipe['image'] = ImagePipe( image=self._image_path, mask=self._mask_path,
255
+ stats=True, abort=self.__abort, address=find_ws_address( ),
256
+ init_script=CustomJS( args=self._mask_add_sub,
257
+ code=self._js['cube-init'] ) if self._mask_path else None )
258
+ if self._pipe['control'] is None:
259
+ ### self._pipe['control']._freeze_cursor_update is used to keep track of whether pixel
260
+ ### update has been "frozen" (by typing 'f')... for "specmode='mfs'" _freeze_cursor_update
261
+ ### was undefined which resulted in failure to update pixel tracking... so it is now
262
+ ### initialized upon construction in JavaScript...
263
+ self._pipe['control'] = DataPipe( address=find_ws_address( ), abort=self.__abort,
264
+ init_script=CustomJS( code='''cb_obj._freeze_cursor_update = false''' ) )
265
+
266
+ def path( self ):
267
+ '''return path to CASA image
268
+ '''
269
+ return self._image_papth
270
+
271
+ def shape( self ):
272
+ '''return shape of image cube
273
+ '''
274
+ self._init_pipes( )
275
+ return self._pipe['image'].shape
276
+
277
+ def fits_header( self ):
278
+ return self._pipe['image'].fits_header( )
279
+
280
+ def channel( self, pixel_type=np.float64 ):
281
+ '''return array for the current channel
282
+ '''
283
+ self._init_pipes( )
284
+ return self._pipe['image'].channel( self._image_source.cur_chan, pixel_type )
285
+
286
+ def jsmask_to_raw( self, all_jsmasks ):
287
+ return all_jsmasks['regions'] if 'regions' in all_jsmasks else all_jsmasks
288
+
289
+ def mask( self ):
290
+ return self._mask_path
291
+
292
+ def mask_id( self ):
293
+ if self._mask_id is None:
294
+ self._mask_id = str(uuid4( ))
295
+ return self._mask_id
296
+
297
+ def set_mask_name( self, new_mask_path ):
298
+ self._mask_path = new_mask_path
299
+ self._pipe['image'].set_mask_name( new_mask_path )
300
+
301
+ def set_all_mask_pixels( self, value ):
302
+ '''Set all pixels to the specified boolean value.
303
+ '''
304
+ shape = self._pipe['image'].shape
305
+ for stokes in range(shape[2]):
306
+ for chan in range(shape[3]):
307
+ mask = self._pipe['image'].mask( [stokes,chan], True )
308
+ mask[:] = 1.0 if value else 0.0
309
+ self._pipe['image'].put_mask( [stokes,chan], mask )
310
+
311
+ def set_channelcb( self, callback ):
312
+ self._channel_callback = callback
313
+
314
+ def _init_image_source( self ):
315
+ if self._image_source is None:
316
+ self._init_pipes( )
317
+ self._image_source = ImageDataSource( image_source=self._pipe['image'] )
318
+
319
+ def image( self, maxanno=50, grid=True, channelcb=None, **kw ):
320
+ '''Create the 2D raster display which displays image planes. This widget is should be
321
+ created for all ``cube_mask`` objects because this is the GUI component that ties
322
+ all of the other GUIs together.
323
+
324
+ Parameters
325
+ ----------
326
+ maxanno: int
327
+ maximum number of masks that can be drawn in each image channel
328
+ grid: Boolean
329
+ display grid lines on the image if True, do not display grid lines if False
330
+ kw: keyword and value
331
+ extra keyword/value paramaters passed on to ``figure``
332
+ '''
333
+ if self._image is None:
334
+
335
+ self._channel_callback = channelcb
336
+
337
+ async def receive_return_value( msg, self=self ):
338
+ self._result = self.jsmask_to_raw( msg['value'] )
339
+ self.__stop( )
340
+ return dict( result='stopped', update={ } )
341
+
342
+ self._init_pipes( )
343
+
344
+ if self._mask_path is None:
345
+ ### create pointer for region center
346
+ self._region_controls['tracking']['pointer'] = PolyAnnotation( xs=[], ys=[], visible=False, editable=False,
347
+ fill_color=self._region_controls['tracking']['color'],
348
+ line_color=self._region_controls['tracking']['color'],
349
+ line_alpha=1.0, hover_line_alpha=1.0,
350
+ fill_alpha=1.0, hover_fill_alpha=1.0 )
351
+
352
+ ### multiple annotations are drawn per channel to create mask from scratch
353
+ self._annotations = [ EvPolyAnnotation( xs=[], ys=[], visible=True, editable=True,
354
+ **self._region_style ) for _ in range(maxanno) ]
355
+ else:
356
+ ### a bitmask cube is available and a single annotation is used to add or subtract from the bitmask cube
357
+ async def mod_mask( msg, self=self ):
358
+ err = None
359
+ shape = self._pipe['image'].shape
360
+ if msg['action'] == 'addition' or msg['action'] == 'subtract':
361
+ if 'xs' in msg['value'] and 'ys' in msg['value']:
362
+ indices = tuple(np.array(list(polygon_indexes( msg['value']['xs'], msg['value']['ys'], shape[:2] ))).T)
363
+ if len(indices) == 0 and len(msg['value']['xs']) > 0 and len(msg['value']['xs']) == len(msg['value']['ys']):
364
+ ### this can happen if the entire region is within a single pixel
365
+ xs = set(map(int,msg['value']['xs']))
366
+ ys = set(map(int,msg['value']['ys']))
367
+ if len(xs) == len(ys) and len(xs) == 1:
368
+ indices = ( np.array([xs.pop()]), np.array([ys.pop( )]) )
369
+ if msg['scope'] == 'chan':
370
+ ### modifying single channel with mouse selected region
371
+ mask = self._pipe['image'].mask( msg['value']['chan'], True )
372
+ mask[indices] = 0 if msg['action'] == 'subtract' else 1
373
+ self._pipe['image'].put_mask( msg['value']['chan'], mask )
374
+ self._mask_id = str(uuid4( )) ### new mask identifier
375
+ return dict( result='success', update={ } )
376
+ elif msg['scope'] == 'cube':
377
+ ### modifying all channels with mouse selected region
378
+ stokes = msg['value']['chan'][0]
379
+ for c in range(shape[3]):
380
+ mask = self._pipe['image'].mask( [stokes,c], True )
381
+ mask[indices] = 0 if msg['action'] == 'subtract' else 1
382
+ self._pipe['image'].put_mask( [stokes,c], mask )
383
+ self._mask_id = str(uuid4( )) ### new mask identifier
384
+ return dict( result='success', update={ } )
385
+ elif 'src' in msg['value']:
386
+ if msg['scope'] == 'chan':
387
+ ### modifying single channel with mask from another channel
388
+ update={ }
389
+ if msg['value']['chan'] == msg['value']['src']:
390
+ if msg['action'] == 'subtract':
391
+ mask = self._pipe['image'].mask( msg['value']['chan'], True )
392
+ mask[:,:] = False
393
+ self._pipe['image'].put_mask( msg['value']['chan'], mask )
394
+ update['clear_region'] = True
395
+ else:
396
+ modifier = self._pipe['image'].mask( msg['value']['src'], True )
397
+ mask = self._pipe['image'].mask( msg['value']['chan'], True )
398
+ if msg['action'] == 'addition':
399
+ mask = np.logical_or( mask, modifier )
400
+ self._pipe['image'].put_mask( msg['value']['chan'], mask )
401
+ else:
402
+ mask = np.logical_and( mask, np.logical_not(modifier) )
403
+ self._pipe['image'].put_mask( msg['value']['chan'], mask )
404
+
405
+ self._mask_id = str(uuid4( )) ### new mask identifier
406
+ return dict( result='success', update=update )
407
+
408
+ elif msg['scope'] == 'cube':
409
+ ### modifying all channels with mask from another channel
410
+ modifier_index = msg['value']['src']
411
+ modifier = self._pipe['image'].mask( modifier_index, True )
412
+ stokes = msg['value']['chan'][0]
413
+ if msg['action'] == 'addition':
414
+ ### addition
415
+ for c in range(shape[3]):
416
+ ### do not add/subtract the modifier mask with itself
417
+ if stokes != modifier_index[0] or c != modifier_index[1]:
418
+ mask = self._pipe['image'].mask( [stokes,c], True )
419
+ mask = np.logical_or( mask, modifier )
420
+ self._pipe['image'].put_mask( [stokes,c], mask )
421
+ else:
422
+ ### subtraction
423
+ for c in range(shape[3]):
424
+ ### do not add/subtract the modifier mask with itself
425
+ if stokes != modifier_index[0] or c != modifier_index[1]:
426
+ mask = self._pipe['image'].mask( [stokes,c], True )
427
+ mask = np.logical_and( mask, np.logical_not(modifier) )
428
+ self._pipe['image'].put_mask( [stokes,c], mask )
429
+ self._mask_id = str(uuid4( )) ### new mask identifier
430
+ return dict( result='success', update={ } )
431
+ else:
432
+ err = "internal error: bad add/subtract scope"
433
+ else:
434
+ err = "internal error: bad add/subtract message"
435
+ elif msg['action'] == 'not':
436
+ notf = np.vectorize(lambda x: 0.0 if x != 0 else 1.0)
437
+ if msg['scope'] == 'chan':
438
+ ### invert single channel
439
+ ### ctrl.send( ids['mask-mod'], { scope: 'chan', action: 'not',
440
+ ### value: { chan: source.cur_chan } },
441
+ ### mask_mod_result )
442
+ mask = self._pipe['image'].mask( msg['value']['chan'], True )
443
+ self._pipe['image'].put_mask( msg['value']['chan'], notf(mask) )
444
+ self._mask_id = str(uuid4( )) ### new mask identifier
445
+ return dict( result='success', update={ } )
446
+ elif msg['scope'] == 'cube':
447
+ ### invert all channels
448
+ ### ctrl.send( ids['mask-mod'], { scope: 'cube', action: 'not',
449
+ ### value: { chan: source.cur_chan } },
450
+ ### mask_mod_result )
451
+ stokes = msg['value']['chan'][0]
452
+ for c in range(shape[3]):
453
+ mask = self._pipe['image'].mask( [stokes,c], True )
454
+ self._pipe['image'].put_mask( [stokes,c], notf(mask) )
455
+ self._mask_id = str(uuid4( )) ### new mask identifier
456
+ return dict( result='success', update={ } )
457
+ else:
458
+ err = "internal error: bad invert scope"
459
+ else:
460
+ err = "internal error: bad message action"
461
+
462
+ return dict( result='failure', update={ }, error=err )
463
+
464
+ ## styling differs between the single "bitmask" PolyAnnotation and the "regions" PolyAnnotation
465
+ self._annotations = [ EvPolyAnnotation( xs=[], ys=[], fill_alpha=1.0, line_color=None, fill_color='black', visible=True, editable=True ) ]
466
+ self._pipe['control'].register( self._ids['mask-mod'], mod_mask )
467
+
468
+
469
+ self._pipe['control'].register( self._ids['done'], receive_return_value )
470
+ self._init_image_source( )
471
+
472
+ ### fetch stokes labels for all stokes drop
473
+ self._stokes_labels = self._image_source.stokes_labels( )
474
+
475
+ self._image = set_attributes( figure( height=self._pipe['image'].shape[1], width=self._pipe['image'].shape[0],
476
+ ###
477
+ ### using webgl resulted in at least one case of unresponsive spans in the colormap
478
+ ### adjust interface due to the GPU being unresponsive (perhaps because it is being
479
+ ### used for something else). In this case, the tclean/deconvolve python session was
480
+ ### on a remote linux system displaying to a mac laptop via X11 (same behavior w/ VNC)
481
+ ###
482
+ #output_backend="webgl",
483
+ match_aspect=True,
484
+ ###
485
+ ### it is required that each tool have a unique "name" to allow for synchronization
486
+ ### of multi-field tool bars in response to changes to the selected tools for one field
487
+ ###
488
+ tools=[ LassoSelectTool(name="Lasso Mask"), BoxSelectTool(name="Box Mask"),
489
+ PanTool(name="Pan"), WheelZoomTool(name="Wheel Zoom"), SaveTool(name="Save"),
490
+ ResetTool(name="Reset"), PolySelectTool(name="Poly Mask") ] +
491
+ ( [ self._mask_add_sub['add'],
492
+ self._mask_add_sub['sub'],
493
+ self._mask_add_sub['mask'] ] if self._mask_path else [ ] ),
494
+ tooltips=None ), **kw )
495
+
496
+ ###
497
+ ### Toggle off grid lines if parameter is False
498
+ ###
499
+ if grid == False:
500
+ self._image.xgrid.grid_line_color = None
501
+ self._image.ygrid.grid_line_color = None
502
+
503
+ ###
504
+ ### set tools that are active by default
505
+ ###
506
+ self._image.toolbar.active_scroll = self._image.select_one(WheelZoomTool)
507
+ self._image.toolbar.active_drag = self._image.select_one(PanTool)
508
+ self._image.toolbar.active_tap = self._image.select_one(PolySelectTool)
509
+
510
+ ###
511
+ ### remove bokeh logo from toolbar
512
+ ###
513
+ self._image.toolbar.logo = None
514
+
515
+ ###
516
+ ### set tick formatting
517
+ ###
518
+ self._image.xaxis.formatter = WcsTicks( axis="x", image_source=self._image_source )
519
+ self._image.yaxis.formatter = WcsTicks( axis="y", image_source=self._image_source )
520
+ self._image.xaxis.major_label_orientation = math.pi/8
521
+
522
+ self._image.x_range.range_padding = self._image.y_range.range_padding = 0
523
+
524
+ shape = self._pipe['image'].shape
525
+ self._chan_image = self._image.image( image="img", x=0, y=0,
526
+ dw=shape[0], dh=shape[1],
527
+ palette=default_palette( True ), level="image",
528
+ source=self._image_source )
529
+ if self._mask_path is not None and path.isdir(self._mask_path):
530
+ ##
531
+ ## LinearColorMapper must be used because otherwise a bitmask that is
532
+ ## all true or all false is colored with self._color by default because
533
+ ## the "image" range drops to a single value, i.e. the maximum value
534
+ ##
535
+ self._bitmask = self._image.image( image='msk', x=0, y=0, dw=shape[0], dh=shape[1],
536
+ color_mapper=LinearColorMapper( low=0, high=1,
537
+ palette=['rgba(0, 0, 0, 0)', self._region_style['fill_color']] ),
538
+ alpha=0.6, source=self._image_source )
539
+ self._bitmask.visible = False
540
+ ###
541
+ ### _bitmask_contour is the contour that is drawn to show the
542
+ ### mask/non-masked boundary of one channel
543
+ ###
544
+ self._bitmask_contour_ds = self._image_source.mask_contour_source( data={ "xs": [ [[[]]] ], "ys": [ [[[]]] ] } )
545
+ self._bitmask_contour = self._image.multi_polygons( xs="xs", ys="ys", fill_color=None, line_color=self._region_style['line_color'],
546
+ source=self._bitmask_contour_ds )
547
+ self._bitmask_contour.visible = True
548
+
549
+ ###
550
+ ### _bitmask_contour_maskmod is the contour that is drawn to represent
551
+ ### the mask/non-masked boundary of one channel for the purpose of
552
+ ### adding or subtracting it from another channel or cube stokes plane
553
+ ###
554
+ self._bitmask_contour_maskmod_ds = ColumnDataSource( data={ "xs": [ [[[]]] ], "ys": [ [[[]]] ] } )
555
+ self._bitmask_contour_maskmod = self._image.multi_polygons( xs="xs", ys="ys", line_width = 3, fill_color=None, line_alpha=0.3,
556
+ line_color=self._region_style['line_color'], line_dash = 'dashed', fill_alpha=0.3,
557
+ source=self._bitmask_contour_maskmod_ds )
558
+ self._bitmask_contour_maskmod.visible = True
559
+
560
+
561
+ if self._pipe['image'].have_mask0( ):
562
+ self._mask0 = self._image.image( image='msk0', x=0, y=0, dw=shape[0], dh=shape[1],
563
+ color_mapper=LinearColorMapper( low=0, high=1,
564
+ palette=['#000000','rgba(0, 0, 0, 0)'] ),
565
+ source=self._image_source )
566
+
567
+ self._image.grid.grid_line_width = 0.5
568
+
569
+ for annotation in self._annotations:
570
+ self._image.add_layout(annotation)
571
+ if self._region_controls['tracking']['pointer']:
572
+ ### only created when multiple regions can be specified
573
+ self._image.add_layout(self._region_controls['tracking']['pointer'])
574
+
575
+ return self._image
576
+
577
+ def goto( self, **kw ):
578
+ if self._goto is None:
579
+ self._init_pipes( )
580
+ self._goto_txt = set_attributes( EvTextInput( value=str(self._slider.start) if self._slider else '0',
581
+ stylesheets=[ InlineStyleSheet( css='''.bk-input { border-bottom-left-radius: 0; border-top-left-radius: 0; margin-left: -0.45em; }''' ) ],
582
+ width=85 ), **kw )
583
+ self._goto_stokes = Dropdown( label="I Channel", menu=self._stokes_labels, stylesheets=[ InlineStyleSheet( css='''.bk-btn { background-color: rgb( 230, 230, 230 ); padding: 7px; padding-top: 8px; border-bottom-right-radius: 0; border-top-right-radius: 0; margin-right: -0.45em; }''' ) ], width=80 )
584
+ self._goto = row( self._goto_stokes, self._goto_txt, spacing=-1 )
585
+
586
+ return self._goto
587
+
588
+ def slider( self, **kw ):
589
+ '''Return slider that is used to change the image plane that is
590
+ displayed on the 2D raster display.
591
+
592
+ Parameters
593
+ ----------
594
+ kw: keyword and value
595
+ extra keyword/value paramaters passed on to ``Slider``
596
+ '''
597
+ if self._slider is None:
598
+ self._init_pipes( )
599
+ shape = self._pipe['image'].shape
600
+ slider_end = shape[-1]-1
601
+ self._slider = set_attributes( Slider( start=0, end=1 if slider_end == 0 else slider_end , value=0, step=1,
602
+ title="Channel" ), **kw )
603
+ if slider_end == 0:
604
+ # for a cube with one channel, a slider is of no use
605
+ self._slider.disabled = True
606
+
607
+ return self._slider
608
+
609
+ def tapedeck( self, **kw ):
610
+ if self._slider is None:
611
+ raise RuntimeError( "tapedeck can only be created after the slider has been created" )
612
+
613
+ stylesheets= [ InlineStyleSheet( css='''.bk-btn { padding-right: 0px; padding-left: 0px; }''' ) ]
614
+ callback=CustomJS( args=dict( slider=self._slider ),
615
+ code='''if ( cb_obj.name == 'forw' )
616
+ if ( cb_obj.name == 'back' ) ''' )
617
+ srt = 0
618
+ end = 3
619
+ fwd = 2
620
+ bck = 1
621
+ self._tapedeck = [ TipButton( icon=svg_icon( [ '20px', 'fast-backward'], **kw ), button_type='light',
622
+ tooltip=Tooltip( content=HTML( 'move to first channel' ), position='bottom' ),
623
+ stylesheets=stylesheets, name='tofront' ),
624
+ TipButton( icon=svg_icon( [ '20px', 'step-backward'], **kw ), button_type='light',
625
+ tooltip=Tooltip( content=HTML( 'move to previous channel' ), position='bottom' ),
626
+ stylesheets=stylesheets, name='back' ),
627
+ TipButton( icon=svg_icon( [ '20px', 'step-forward'], **kw ), button_type='light',
628
+ tooltip=Tooltip( content=HTML( 'move to next channel' ), position='bottom' ),
629
+ stylesheets=stylesheets, name='forw' ),
630
+ TipButton( icon=svg_icon( [ '20px', 'fast-forward'], **kw ), button_type='light',
631
+ tooltip=Tooltip( content=HTML( 'move to last channel' ), position='bottom' ),
632
+ stylesheets=stylesheets, name='toend' ) ]
633
+
634
+ self._tapedeck[fwd].js_on_click( CustomJS( args=dict( slider=self._slider ),
635
+ code='''slider.value = slider.value == slider.end ? slider.start : slider.value + 1''' ) )
636
+ self._tapedeck[bck].js_on_click( CustomJS( args=dict( slider=self._slider ),
637
+ code='''slider.value = slider.value == slider.start ? slider.end : slider.value - 1''' ) )
638
+ self._tapedeck[end].js_on_click( CustomJS( args=dict( slider=self._slider ),
639
+ code='''slider.value = slider.end''' ) )
640
+ self._tapedeck[srt].js_on_click( CustomJS( args=dict( slider=self._slider ),
641
+ code='''slider.value = slider.start''' ) )
642
+
643
+ return row( *self._tapedeck )
644
+
645
+ def spectrum( self, orient='horizontal', **kw ):
646
+ '''Return the line graph of spectrum from the image cube which is updated
647
+ in response to moving the cursor within the 2D raster display.
648
+
649
+ Parameters
650
+ ----------
651
+ kw: keyword and value
652
+ extra keyword/value paramaters passed on to ``figure``
653
+ '''
654
+ if self._spectrum is None:
655
+ if self._image is None:
656
+ ###
657
+ ### an exception is raised instead of just creating the image display because if we create
658
+ ### it here [by calling self.image( )], the user will silently lose the ability to set the
659
+ ### maximum number of annotations per channel (along with other future parameters)
660
+ ###
661
+ raise RuntimeError('spectrum( ) requires an image cube display, but one has not yet been created')
662
+
663
+ nelem = self._pipe['image'].shape[-1]
664
+ self._image_spectrum = ColumnDataSource( data={ 'chan': list(range(nelem)), 'pixel': [0] * nelem } )
665
+
666
+ self._sp_span = Span( location=-1,
667
+ dimension='width' if orient == 'vertical' else 'height',
668
+ line_color='slategray',
669
+ line_width=2,
670
+ visible=False )
671
+
672
+ self._cb['sppos'] = CustomJS( args=dict( span=self._sp_span,vertical=orient != 'vertical' ),
673
+ code = """var geometry = cb_data['geometry'];
674
+ var x_pos = Math.round(geometry.x);
675
+ var y_pos = Math.round(geometry.y);
676
+ if ( isFinite(x_pos) && isFinite(y_pos) ) {
677
+ span.visible = true
678
+ span.location = vertical ? x_pos : y_pos
679
+ } else {
680
+ span.visible = false
681
+ span.location = -1
682
+ }""" )
683
+
684
+ self._hover['spectrum'] = HoverTool( callback=self._cb['sppos'] )
685
+
686
+ self._spectrum = set_attributes( figure( tools=[ self._hover['spectrum'] ] ), **kw )
687
+ self._spectrum.add_layout(self._sp_span)
688
+
689
+ self._spectrum.x_range.range_padding = self._spectrum.y_range.range_padding = 0
690
+ self._spectrum.line( x='pixel' if orient == 'vertical' else 'chan', y='chan' if orient == 'vertical' else 'pixel', source=self._image_spectrum )
691
+ self._spectrum.grid.grid_line_width = 0.5
692
+
693
+ return self._spectrum
694
+
695
+ def coorddesc( self ):
696
+ return self._pipe['image'].coorddesc( )
697
+
698
+ def statistics( self, **kw ):
699
+ '''retrieve a DataTable which is updated in response to changes in the
700
+ image cube display
701
+ '''
702
+ if self._statistics is None:
703
+ image_stats = self._pipe['image'].statistics( [0,0] )
704
+ self._statistics_source = ColumnDataSource( { 'labels': list(image_stats.keys( )),
705
+ 'values': list(image_stats.values( )) } )
706
+
707
+ stats_column = [ TableColumn(field='labels', title='Statistics', width=75),
708
+ TableColumn(field='values', title='Values') ]
709
+
710
+ # using set_attributes allows the user to override defaults like 'width=400'
711
+ self._statistics = set_attributes( DataTable( source=self._statistics_source, columns=stats_column, index_position=None ), **kw )
712
+ #self._statistics = set_attributes( DataTable( source=self._statistics_source, columns=stats_column,
713
+ # height_policy='fit' ), **kw )
714
+ #height_policy='fit' ), **kw )
715
+ #width=400, height=200, height_policy='fit' ), **kw )
716
+ #width=400, height=200, sizing_mode='stretch_height' ), **kw )
717
+ #width=400, height=200, height_policy='max' ), **kw )
718
+ #width=400, height=200, autosize_mode='none', height_policy='max' ), **kw )
719
+ #width=400, height=200, autosize_mode='none', sizing_mode='stretch_height' ), **kw )
720
+ #width=400, height=200, autosize_mode='none' ), **kw )
721
+ if self._mask_path:
722
+ async def config_statistics( msg, self=self ):
723
+ if 'value' in msg and self._statistics_use_mask != bool(msg['value']):
724
+ self._statistics_use_mask = bool(msg['value'])
725
+ self._pipe['image'].statistics_config( use_mask=self._statistics_use_mask )
726
+ return dict( result='OK', update={ } )
727
+ else:
728
+ return dict( result='NOP', update={ } )
729
+
730
+ self._pipe['control'].register( self._ids['config-statistics'], config_statistics )
731
+ self._statistics_mask = Dropdown( label="Channel Statistics", button_type='light', margin=(5,0,-1,0),
732
+ menu=[ 'Channel Statistics', 'Mask Statistics' ],
733
+ css_classes=['cg-btn-selector'] )
734
+
735
+ if self._mask_path:
736
+ return ( self._statistics_mask, self._statistics )
737
+ else:
738
+ return ( self._statistics, )
739
+
740
+ def palette( self, reuse=None, **kw ):
741
+ '''retrieve a Select widget which allow for changing the pseudocolor palette
742
+ '''
743
+ if self._palette is None:
744
+ if self._image is None:
745
+ ###
746
+ ### an exception is raised instead of just creating the image display because if we create
747
+ ### it here [by calling self.image( )], the user will silently lose the ability to set the
748
+ ### maximum number of annotations per channel (along with other future parameters)
749
+ ###
750
+ raise RuntimeError('palette( ) requires an image cube display, but one has not yet been created')
751
+
752
+ async def fetch_palette( msg, self=self ):
753
+ if 'value' in msg:
754
+ return dict( result=find_palette(msg['value']), value=msg['value'], update={ } )
755
+ else:
756
+ return dict( result=None, value=None, update={ } )
757
+
758
+ self._pipe['control'].register( self._ids['palette'], fetch_palette )
759
+
760
+ if reuse:
761
+ self._palette = reuse.child
762
+ else:
763
+ self._palette = set_attributes( Dropdown( label=default_palette( ), button_type='light', margin=(-1, 0, 0, 0),
764
+ sizing_mode='scale_height', menu=available_palettes( ) ), **kw )
765
+
766
+ self._palette.js_on_click( CustomJS( args=dict( image=self._chan_image,
767
+ ids=self._ids,
768
+ ctrl=self._pipe['control'] ),
769
+ code='''function receive_palette( msg ) {
770
+ if ( 'result' in msg && msg.result != null ) {
771
+ let cm = image.glyph.color_mapper
772
+ cm.palette = msg.result
773
+ cm.change.emit( )
774
+ cb_obj.origin.label = msg.value
775
+ }
776
+ }
777
+ ctrl.send( ids['palette'],
778
+ { action: 'palette', value: this.item },
779
+ receive_palette )''' ) )
780
+
781
+ return Tip( self._palette, tooltip=Tooltip( content=HTML("Select the colormap used to render the image cube"), position="right" ) )
782
+
783
+
784
+ def colormap_adjust( self, **kw ):
785
+
786
+ chan = self.channel( )
787
+ bins = np.linspace( chan.min( ), chan.max( ), self._cm_adjust['bins'] )
788
+ hist, edges = np.histogram( chan, density=False, bins=bins )
789
+
790
+ span_edited_funcs = '''function set_edited( span ) {
791
+ if (typeof span._original_dash == 'undefined')
792
+ span._original_dash = span.line_dash
793
+ span.line_dash = [ ]
794
+ span._edited = true
795
+ }
796
+ function clear_edited( span ) {
797
+ if (typeof span._original_dash != 'undefined')
798
+ span.line_dash = span._original_dash
799
+ span._edited = false
800
+ }
801
+ '''
802
+
803
+ self._cm_adjust['span one'] = EditSpan( location=edges[0], dimension='height', line_color='red', line_width=1,
804
+ editable=True, line_dash='dashed' )
805
+ self._cm_adjust['span two'] = EditSpan( location=edges[-1], dimension='height', line_color='red', line_width=1,
806
+ editable=True, line_dash='dashed' )
807
+
808
+ ###
809
+ ### Bokeh supports 'description=Tooltip( content=HTML("..."), position="..." )'. However,
810
+ ### The Tooltip(...) works by creating an "i" in a circle with the label that can be clicked.
811
+ ### With "prefix=..." and no label, no button is displayed.
812
+ ###
813
+ self._cm_adjust['min input'] = TextInput( value=str(edges[0]), prefix="min" )
814
+ self._cm_adjust['min input'].js_on_event( ValueSubmit, CustomJS( args=dict( span1=self._cm_adjust['span one'],
815
+ span2=self._cm_adjust['span two'] ),
816
+ code=span_edited_funcs +
817
+ '''if ( span1.location <= span2.location ) {
818
+ span1._refresh_colormap = true
819
+ span1.location = Number(cb_obj.origin.value)
820
+ set_edited(span1)
821
+ } else {
822
+ span2._refresh_colormap = true
823
+ span2.location = Number(cb_obj.origin.value)
824
+ set_edited(span2)
825
+ }''' ) )
826
+
827
+ self._cm_adjust['max input'] = TextInput( value=str(edges[-1]), prefix="max" )
828
+ self._cm_adjust['max input'].js_on_event( ValueSubmit, CustomJS( args=dict( span1=self._cm_adjust['span one'],
829
+ span2=self._cm_adjust['span two'] ),
830
+ code=span_edited_funcs +
831
+ '''if ( span1.location >= span2.location ) {
832
+ span1._refresh_colormap = true
833
+ span1.location = Number(cb_obj.origin.value)
834
+ set_edited(span1)
835
+ } else {
836
+ span2._refresh_colormap = true
837
+ span2.location = Number(cb_obj.origin.value)
838
+ set_edited(span2)
839
+ }''' ) )
840
+
841
+ self._cm_adjust['reset'] = CBResetTool( icon=casaimage.as_mime(join( dirname(dirname(__file__)), "__icons__", 'reset.png' )),
842
+ description="Reset pan/zoom and extents" )
843
+ self._cm_adjust['fig'] = figure( width=250, height=200, toolbar_location='above',
844
+ tools=[ self._cm_adjust['reset'],
845
+ # see https://github.com/bokeh/bokeh/pull/13593
846
+ # and https://github.com/bokeh/bokeh/issues/12486
847
+ #WheelZoomTool(toggleable=False), PanTool(toggleable=False),
848
+ #WheelZoomTool(visible=False), PanTool(toggleable=False),
849
+ 'wheel_zoom', 'pan',
850
+ ResetTool( icon=casaimage.as_mime(join( dirname(dirname(__file__)), "__icons__", 'zoom-to-fit.png' )),
851
+ description="Reset pan/zoom but preserve extents" ) ],
852
+ sizing_mode="scale_width" )
853
+
854
+ self._cm_adjust['fig'].toolbar.active_scroll = self._cm_adjust['fig'].select_one(WheelZoomTool)
855
+
856
+ ###
857
+ ### remove bokeh logo from toolbar
858
+ ### due to a bokeh bug, currently removing the bokeh logo pushes one of the tools into
859
+ ### the ellipsis at the right of the GUI... Mon Jul 8 14:54:01 EDT 2024
860
+ ###
861
+ #self._cm_adjust['fig'].toolbar.logo = None
862
+
863
+ # Create a new BasicTickFormatter
864
+ formatter = BasicTickFormatter()
865
+
866
+ # Set the number of decimal places to display
867
+ formatter.precision = 1
868
+
869
+ self._cm_adjust['fig'].yaxis.formatter = formatter
870
+ self._cm_adjust['fig'].renderers.extend([self._cm_adjust['span one'], self._cm_adjust['span two']])
871
+
872
+ self._cm_adjust['hist-ds'] = self._pipe['image'].histogram_source( data=dict( left=list(edges[:-1]), right=list(edges[1:]), top=list(hist), bottom=[0]*len(hist) ) )
873
+ self._cm_adjust['hist-glyph'] = Quad( left="left", right="right", top="top", bottom=0, fill_color="blue", line_color="blue" )
874
+ self._cm_adjust['histogram'] = self._cm_adjust['fig'].add_glyph( self._cm_adjust['hist-ds'], self._cm_adjust['hist-glyph'] )
875
+
876
+ ### linear: 𝑦=𝑥
877
+ ### log: 𝑦=log𝛼𝑥+1(𝛼𝑥+1) == Math.log(alpha * x + 1.0) / Math.log(alpha + 1.0)
878
+ ### square root: 𝑦=√x
879
+ ### square: 𝑦=𝑥^2
880
+ ### gamma: 𝑦=𝑥^𝛾
881
+ ### power: 𝑦=(𝛼^𝑥−1)/(𝛼−1)
882
+ self._cm_adjust['alpha-value'] = TextInput( value="1000", prefix="alpha", max_width=170, visible=False )
883
+ self._cm_adjust['gamma-value'] = TextInput( value="1", prefix="gamma", max_width=170, visible=False )
884
+ self._cm_adjust['equation'] = Div(text='''<math><mrow><mi>y</mi><mo>=</mo><mi>x</mi></mrow></math>''') # linear
885
+ self._cm_adjust['scaling'] = Dropdown( label='linear',
886
+ menu=[ ('linear', 'linear'), ('log', 'log'), ('sqrt','sqrt'),
887
+ ('square', 'square'), ('gamma', 'gamma'), ('power', 'power') ],
888
+ button_type='light' )
889
+
890
+ colormap_refresh_code = '''let args = { }
891
+ if ( alpha.visible ) args = { alpha: parseFloat(alpha.value), ...args }
892
+ if ( gamma.visible ) args = { gamma: parseFloat(gamma.value), ...args }
893
+ const [ minspan, maxspan ] = span1.location <= span2.location ? [ span1, span2 ] : [ span2, span1 ]
894
+ source.adjust_colormap( [ minspan._edited ? [ minspan.location ] : [ ],
895
+ maxspan._edited ? [ maxspan.location ] : [ ] ],
896
+ { scaling: scaling.label, args }, msg => { source.refresh( ) } )'''
897
+
898
+ ###
899
+ ### "( span1._editing && span2._editing )" update happens when the
900
+ ### one of the spans is being dragged. Image is updated here when
901
+ ### the LODEnd event is received
902
+ ###
903
+ ### Otherwise the only time this should be called is in responce to
904
+ ### either the min or max text input being changed directly. In this
905
+ ### case the image is updated as a result of the text input change.
906
+ ###
907
+ span_cb = '''if ( span1._editing || span2._editing ) {
908
+ min.value = (Math.min(span1.location,span2.location)).toString( )
909
+ max.value = (Math.max(span1.location,span2.location)).toString( )
910
+ }
911
+ if ( cb_obj._refresh_colormap ) {
912
+ cb_obj._refresh_colormap = false
913
+ %s
914
+ }'''
915
+
916
+ self._cm_adjust['span one'].js_on_change( 'location', CustomJS( args=dict( source=self._image_source,
917
+ min=self._cm_adjust['min input'],
918
+ max=self._cm_adjust['max input'],
919
+ span1=self._cm_adjust['span one'],
920
+ span2=self._cm_adjust['span two'],
921
+ scaling=self._cm_adjust['scaling'],
922
+ alpha=self._cm_adjust['alpha-value'],
923
+ gamma=self._cm_adjust['gamma-value'],
924
+ equation=self._cm_adjust['equation'] ),
925
+ code=span_cb % colormap_refresh_code ) )
926
+ self._cm_adjust['span two'].js_on_change( 'location', CustomJS( args=dict( source=self._image_source,
927
+ min=self._cm_adjust['min input'],
928
+ max=self._cm_adjust['max input'],
929
+ span1=self._cm_adjust['span one'],
930
+ span2=self._cm_adjust['span two'],
931
+ scaling=self._cm_adjust['scaling'],
932
+ alpha=self._cm_adjust['alpha-value'],
933
+ gamma=self._cm_adjust['gamma-value'],
934
+ equation=self._cm_adjust['equation'] ),
935
+ code=span_cb % colormap_refresh_code ) )
936
+
937
+ self._cm_adjust['span one'].js_on_event( LODStart, CustomJS( code= span_edited_funcs +
938
+ '''cb_obj.origin._editing = true
939
+ set_edited( cb_obj.origin )''' ) )
940
+ self._cm_adjust['span one'].js_on_event( LODEnd, CustomJS( args=dict( source=self._image_source,
941
+ min=self._cm_adjust['min input'],
942
+ max=self._cm_adjust['max input'],
943
+ span1=self._cm_adjust['span one'],
944
+ span2=self._cm_adjust['span two'],
945
+ scaling=self._cm_adjust['scaling'],
946
+ alpha=self._cm_adjust['alpha-value'],
947
+ gamma=self._cm_adjust['gamma-value'],
948
+ equation=self._cm_adjust['equation'] ),
949
+ code='''cb_obj.origin._editing = false;'''+colormap_refresh_code ) )
950
+ self._cm_adjust['span two'].js_on_event( LODStart, CustomJS( code= span_edited_funcs +
951
+ '''cb_obj.origin._editing = true
952
+ set_edited( cb_obj.origin )''' ) )
953
+ self._cm_adjust['span two'].js_on_event( LODEnd, CustomJS( args=dict( source=self._image_source,
954
+ min=self._cm_adjust['min input'],
955
+ max=self._cm_adjust['max input'],
956
+ span1=self._cm_adjust['span one'],
957
+ span2=self._cm_adjust['span two'],
958
+ scaling=self._cm_adjust['scaling'],
959
+ alpha=self._cm_adjust['alpha-value'],
960
+ gamma=self._cm_adjust['gamma-value'],
961
+ equation=self._cm_adjust['equation'] ),
962
+ code='''cb_obj.origin._editing = false;'''+colormap_refresh_code ) )
963
+
964
+
965
+ update_scaling_state = '''alpha.visible = false
966
+ gamma.visible = false
967
+ if ( scaling.label == 'linear' ) {
968
+ equation.text = '<math><mrow><mi>y</mi><mo>=</mo><mi>x</mi></mrow></math>'
969
+ } else if ( scaling.label == 'log' ) {
970
+ alpha.visible = true
971
+ equation.text = '<math><mrow><mi>y</mi><mo>=</mo><msub><mi>log</mi><mrow><mn>&alpha;</mn><mo>+</mo><mn>1</mn></mrow></msub><mrow><mo>(</mo><mn>&alpha;</mn><mn>x</mn></mrow><mo>+</mo><mn>1</mn><mo>)</mo></mrow></mrow></math>'
972
+ } else if ( scaling.label == 'sqrt' ) {
973
+ equation.text = '<math><mrow><mi>y</mi><mo>=</mo><msqrt><mi>x</mi></msqrt></mrow></math>'
974
+ } else if ( scaling.label == 'square' ) {
975
+ equation.text = '<math><mrow><mi>y</mi><mo>=</mo><msup><mi>x</mi><mn>2</mn></msup></mrow></math>'
976
+ } else if ( scaling.label == 'gamma' ) {
977
+ gamma.visible = true
978
+ equation.text = '<math><mrow><mi>y</mi><mo>=</mo><msup><mi>x</mi><mn>&gamma;</mn></msup></mrow></math>'
979
+ } else if ( scaling.label == 'power' ) {
980
+ alpha.visible = true
981
+ equation.text = '<math><mrow><mi>y</mi><mo>=</mo><msub><mi>log</mi><mrow><mn>&alpha;</mn><mo>+</mo><mn>1</mn></mrow></msub><mrow><mo>(</mo><msup><mn>&alpha;</mn><mn>x</mn></msup><mo>+</mo><mn>1</mn><mo>)</mo></mrow></mrow></math>'
982
+ } else if ( scaling.label == 'power' ) {
983
+ alpha.visible = true
984
+ equation.text = '<math><mrow><mi>y</mi><mo>=</mo><mfrac><mrow><msup><mn>&alpha;</mn><mi>x</mi></msup><mo>-</mo><mn>1</mn></mrow><mrow><mn>&alpha;</mn><mo>-</mo><mn>1</mn></mrow></mfrac></mrow></math>'
985
+ } else {
986
+ equation.text = scaling.label
987
+ }'''
988
+
989
+ self._cm_adjust['reset'].postcallback = CustomJS( args=dict( span1=self._cm_adjust['span one'],
990
+ span2=self._cm_adjust['span two'],
991
+ mintxt=self._cm_adjust['min input'],
992
+ maxtxt=self._cm_adjust['max input'],
993
+ source=self._image_source,
994
+ histogram=self._cm_adjust['histogram'],
995
+ scaling=self._cm_adjust['scaling'],
996
+ alpha=self._cm_adjust['alpha-value'],
997
+ gamma=self._cm_adjust['gamma-value'],
998
+ equation=self._cm_adjust['equation'] ),
999
+ code=span_edited_funcs +
1000
+ '''scaling.label = scaling.menu[0][0]
1001
+ %s
1002
+ scaling.change.emit( )
1003
+ span1.location = histogram.data_source.data.left[0]
1004
+ clear_edited(span1)
1005
+ span2.location = histogram.data_source.data.right[histogram.data_source.data.right.length-1]
1006
+ clear_edited(span2)
1007
+ mintxt.value = span1.location.toString( )
1008
+ maxtxt.value = span2.location.toString( )
1009
+ %s''' % ( update_scaling_state, colormap_refresh_code ) )
1010
+
1011
+ self._cm_adjust['scaling'].js_on_event( "menu_item_click", CustomJS( args=dict( span1=self._cm_adjust['span one'],
1012
+ span2=self._cm_adjust['span two'],
1013
+ mintxt=self._cm_adjust['min input'],
1014
+ maxtxt=self._cm_adjust['max input'],
1015
+ source=self._image_source,
1016
+ histogram=self._cm_adjust['histogram'],
1017
+ scaling=self._cm_adjust['scaling'],
1018
+ alpha=self._cm_adjust['alpha-value'],
1019
+ gamma=self._cm_adjust['gamma-value'],
1020
+ equation=self._cm_adjust['equation'] ),
1021
+ code='''if ( cb_obj.item != cb_obj.origin.label ) {
1022
+ scaling.label = cb_obj.item
1023
+ %s
1024
+ %s
1025
+ }''' % ( update_scaling_state, colormap_refresh_code )) )
1026
+
1027
+ scaling_parameter_callback = CustomJS( args=dict( span1=self._cm_adjust['span one'],
1028
+ span2=self._cm_adjust['span two'],
1029
+ mintxt=self._cm_adjust['min input'],
1030
+ maxtxt=self._cm_adjust['max input'],
1031
+ source=self._image_source,
1032
+ histogram=self._cm_adjust['histogram'],
1033
+ scaling=self._cm_adjust['scaling'],
1034
+ alpha=self._cm_adjust['alpha-value'],
1035
+ gamma=self._cm_adjust['gamma-value'],
1036
+ equation=self._cm_adjust['equation'] ),
1037
+ code=colormap_refresh_code )
1038
+
1039
+ self._cm_adjust['alpha-value'].js_on_change( 'value', scaling_parameter_callback )
1040
+ self._cm_adjust['gamma-value'].js_on_change( 'value', scaling_parameter_callback )
1041
+
1042
+ async def colormap_adjust_update( msg, self=self ):
1043
+ if 'action' in msg and msg['action'] == 'fetch':
1044
+ chan = self.channel( )
1045
+ bins = np.linspace( chan.min( ), chan.max( ), self._cm_adjust['bins'] )
1046
+ hist, edges = np.histogram( chan, density=False, bins=bins )
1047
+ return dict( result='success', hist=hist, edges=edges )
1048
+
1049
+ return dict( result='failure', update={ } )
1050
+
1051
+ return column( self._cm_adjust['fig'],
1052
+ row( Tip( self._cm_adjust['min input'],
1053
+ tooltip=Tooltip( content=HTML("set minimum clip here or drag the left red line above"),
1054
+ position="top" ) ),
1055
+ Tip( self._cm_adjust['max input'],
1056
+ tooltip=Tooltip( content=HTML("set maximum clip here or drag the right red line above"),
1057
+ position="top_left" ) ), width_policy='min' ),
1058
+ row( Tip( self._cm_adjust['scaling'],
1059
+ tooltip=Tooltip( content=HTML('scaling function applied to image intensities'),
1060
+ position="top" ) ),
1061
+ self._cm_adjust['equation'] ),
1062
+ row( Tip( self._cm_adjust['alpha-value'],
1063
+ tooltip=Tooltip( content=HTML('set alpha value as indicated in the equation'),
1064
+ position="top" ) ),
1065
+ Tip( self._cm_adjust['gamma-value'],
1066
+ tooltip=Tooltip( content=HTML('set gamma value as indicated in the equation'),
1067
+ position="top" ) ) ),
1068
+ sizing_mode="scale_width" )
1069
+
1070
+ def bitmask_ctrl( self, reuse=None, **kw ):
1071
+
1072
+ if self._bitmask is None:
1073
+ raise RuntimeError('CubeMask: bitmask cube not in use')
1074
+
1075
+ ###
1076
+ ### retrieve controls for adjusting the cube bitmask
1077
+ ###
1078
+ ### NOTE: the self._bitmask_color_selector change function is setup
1079
+ ### in the "connect" member function
1080
+ ###
1081
+
1082
+
1083
+ ### widget to select how the masked area should be indicated:
1084
+ ###
1085
+ ### contour -- draw a dashed line at the transition between ones and zeros in the mask
1086
+ ### masked -- shade the area that is masked
1087
+ ### unmasked -- shade the area that is unmasked
1088
+ ###
1089
+ if reuse is not None and reuse[0] is not None:
1090
+ self._bitmask_transparency_button = reuse[0].child
1091
+ else:
1092
+ self._bitmask_transparency_button = set_attributes( Dropdown( label='contour', button_type='light', margin=(-1, 0, 0, 0),
1093
+ sizing_mode='scale_height', menu=['contour','masked','unmasked'] ), **kw )
1094
+
1095
+ ###
1096
+ ### color to be used when drawing the contour or masked/unmasked shading
1097
+ ###
1098
+ if reuse is not None and reuse[1] is not None:
1099
+ self._bitmask_color_selector = reuse[1].child
1100
+ else:
1101
+ self._bitmask_color_selector = ColorPicker( width_policy='fixed', width=40, color=self._region_style['fill_color'], margin=(-1, 0, 0, 0),
1102
+ stylesheets=[ InlineStyleSheet( css='''.bk-input { border: 0px solid #ccc;
1103
+ padding: 0 var(--padding-vertical); }''' ) ] )
1104
+
1105
+ ###
1106
+ ### transparency to be used when shading the masked/unmasked area
1107
+ ###
1108
+ if reuse is not None and reuse[2] is not None:
1109
+ mask_alpha_pick = reuse[2].child
1110
+ else:
1111
+ mask_alpha_pick = Spinner( width_policy='fixed', width=55, low=0.0, high=1.0, mode='float', step=0.1, value=0.6, margin=(-1, 0, 0, 0), visible=False )
1112
+
1113
+ mask_alpha_pick.js_on_change( 'value', CustomJS( args=dict( bitmask=self._bitmask ),
1114
+ code='''let gl = bitmask.glyph
1115
+ gl.global_alpha.value = cb_obj.value
1116
+ gl.change.emit( )''' ) )
1117
+
1118
+ ###
1119
+ ### created above but callback uses mask_alpha_pick
1120
+ ###
1121
+ self._bitmask_transparency_button.js_on_click( CustomJS( args=dict( bitmask=self._bitmask, contour=self._bitmask_contour,
1122
+ contour_ds=self._bitmask_contour_ds,
1123
+ selector=self._bitmask_color_selector,
1124
+ alpha=mask_alpha_pick ),
1125
+ code='''let cm = bitmask.glyph.color_mapper
1126
+ if ( bitmask._transparent == null ) {
1127
+ bitmask._transparent = cm.palette[0]
1128
+ }
1129
+ if ( this.item == 'masked' ) {
1130
+ cm.palette[0] = bitmask._transparent
1131
+ cm.palette[1] = selector.color
1132
+ contour.visible = false
1133
+ bitmask.visible = true
1134
+ alpha.visible = true
1135
+ cm.change.emit( )
1136
+ } else if ( this.item == 'unmasked' ) {
1137
+ cm.palette[0] = selector.color
1138
+ cm.palette[1] = bitmask._transparent
1139
+ contour.visible = false
1140
+ bitmask.visible = true
1141
+ alpha.visible = true
1142
+ cm.change.emit( )
1143
+ } else if ( this.item == 'contour' ) {
1144
+ contour.visible = true
1145
+ bitmask.visible = false
1146
+ alpha.visible = false
1147
+ }
1148
+ this.origin.label = this.item''' ) )
1149
+
1150
+ return ( Tip( self._bitmask_transparency_button,
1151
+ tooltip=Tooltip( content=HTML("The mask can be displayed as a <b>contour</b> or the <b>masked/unmasked</b> portion can be shaded"),
1152
+ position='right' ) ),
1153
+ Tip( self._bitmask_color_selector,
1154
+ tooltip=Tooltip( content=HTML("Set the color used for drawing the mask"), position="right" ) ),
1155
+ Tip( mask_alpha_pick,
1156
+ tooltip=Tooltip( content=HTML("<b>If</b> the mask is indicated with shading, this sets the opaqueness of the shading"),
1157
+ position="bottom" ) ) )
1158
+
1159
+ def region_position_ctrl( self ):
1160
+
1161
+ self.COUNT = self.COUNT + 1
1162
+
1163
+ if self._mask_path is not None:
1164
+ raise RuntimeError( 'only applicable for region creation when a bitmask is not used' )
1165
+
1166
+ if not self._region_controls['coord']['initialized'] and self._mask_path is None:
1167
+ self._region_controls['coord']['initialized'] = True
1168
+
1169
+ self._region_controls['coord']['sx'] = Tip( TextInput( value='', disabled=True ),
1170
+ tooltip=Tooltip( content=HTML("Set X (screen) coordinate for center of selected region"),
1171
+ position="bottom" ) )
1172
+ self._region_controls['coord']['sy'] = Tip( TextInput( value='', disabled=True ),
1173
+ tooltip=Tooltip( content=HTML("Set Y (screen) coordinate for center of selected region"),
1174
+ position="bottom" ) )
1175
+ self._region_controls['coord']['wx'] = Tip( TextInput( value='', disabled=True ),
1176
+ tooltip=Tooltip( content=HTML("Set X (world) coordinate for center of selected region"),
1177
+ position="bottom" ) )
1178
+ self._region_controls['coord']['wy'] = Tip( TextInput( value='', disabled=True ),
1179
+ tooltip=Tooltip( content=HTML("Set Y (world) coordinate for center of selected region"),
1180
+ position="bottom" ) )
1181
+ self._region_controls['coord']['label'] = Tip( TextInput( value='', prefix="Label", disabled=True ),
1182
+ tooltip=Tooltip( content=HTML("Label associated with this polygon"), position="bottom" ) )
1183
+ self._region_controls['coord']['chan'] = { s: Tip( TextInput( value='', sizing_mode='stretch_width', disabled=True ),
1184
+ tooltip=Tooltip( content=HTML(f'''<p>Channels ({s}) which should include the selected region</p>
1185
+ <p>For ranges use "start:end" separated by commas</p>'''),
1186
+ position="bottom" ), sizing_mode='stretch_width' )
1187
+ for s in self._stokes_labels }
1188
+ self._region_controls['coord']['status'] = Div(text='')
1189
+
1190
+ self._region_controls['tracking']['color-picker'] = Tip( ColorPicker( color=self._region_controls['tracking']['color'], width=40 ),
1191
+ tooltip=Tooltip( content=HTML("Color used for tracking dragging or resizing"), position="bottom" ) )
1192
+ self._region_controls['tracking']['enable'] = Tip( Dropdown( label='enable',
1193
+ menu=[ ('enable','enable'), ('disable','disable') ],
1194
+ button_type='light' ),
1195
+ tooltip=Tooltip( content=HTML("Disable center tracking when dragging or resizing regions"),
1196
+ position="bottom" ) )
1197
+
1198
+ return { 'pixel': ( self._region_controls['coord']['sx'],
1199
+ self._region_controls['coord']['sy'] ),
1200
+ 'world': ( self._region_controls['coord']['wx'],
1201
+ self._region_controls['coord']['wy'] ),
1202
+ 'chan': self._region_controls['coord']['chan'],
1203
+ 'status': self._region_controls['coord']['status'],
1204
+ 'label': self._region_controls['coord']['label'],
1205
+ 'tracking': ( self._region_controls['tracking']['enable'],
1206
+ self._region_controls['tracking']['color-picker'] ) }
1207
+
1208
+ def region_style_ctrl( self, reuse=None, **kw ):
1209
+
1210
+ def create_region_control( control_type ):
1211
+
1212
+ if control_type == 'hover':
1213
+ pfx = 'hover_'
1214
+ else:
1215
+ pfx = ''
1216
+
1217
+ if control_type not in self._region_controls['style']:
1218
+
1219
+ self._region_controls['style'][control_type] = dict(fill={ }, line={ })
1220
+
1221
+ if reuse is None:
1222
+ self._region_controls['style'][control_type]['fill']['color'] = Tip( ColorPicker( width_policy='fixed', width=40,
1223
+ color=self._region_style[f'''{pfx}fill_color'''],
1224
+ margin=(-1, 0, 0, 0),
1225
+ stylesheets=[ InlineStyleSheet( css='''.bk-input { border: 0px solid #ccc;
1226
+ padding: 0 var(--padding-vertical); }''' ) ] ),
1227
+ tooltip=Tooltip( content=HTML(self._region_style_tips[f'''{control_type}_fill_color''']),
1228
+ position="bottom" ) )
1229
+ self._region_controls['style'][control_type]['fill']['alpha'] = Tip( Spinner( width_policy='fixed', width=55, low=0.0, high=1.0, mode='float', step=0.1,
1230
+ value=self._region_style['fill_alpha'], margin=(-1, 0, 0, 0) ),
1231
+ tooltip=Tooltip( content=HTML(self._region_style_tips[f'''{control_type}_fill_alpha''']),
1232
+ position="bottom" ) )
1233
+ self._region_controls['style'][control_type]['fill']['hatch'] = Tip( set_attributes( Dropdown( label=self._region_style[f'''{pfx}hatch_pattern'''],
1234
+ button_type='light', margin=(-1, 0, 0, -1),
1235
+ width=130, menu=[h.replace('_',' ') for h in _hatches] ), **kw ),
1236
+ tooltip=Tooltip( content=HTML(self._region_style_tips[f'''{control_type}_hatch_pattern''']),
1237
+ position="bottom" ) )
1238
+ self._region_controls['style'][control_type]['line']['color'] = Tip( ColorPicker( width_policy='fixed', width=40, color=self._region_style[f'''{pfx}line_color'''],
1239
+ margin=(-1, 0, 0, 0 ),
1240
+ stylesheets=[ InlineStyleSheet( css='''.bk-input { border: 0px solid #ccc;
1241
+ padding: 0 var(--padding-vertical); }''' ) ] ),
1242
+ tooltip=Tooltip( content=HTML(self._region_style_tips[f'''{control_type}_line_color''']),
1243
+ position="bottom" ) )
1244
+ self._region_controls['style'][control_type]['line']['width'] = Tip( Spinner( width_policy='fixed', width=55, low=0, high=10, mode='float', step=0.1,
1245
+ value=self._region_style[f'''{pfx}line_width'''], margin=(-1, 0, 0, 0) ),
1246
+ tooltip=Tooltip( content=HTML(self._region_style_tips[f'''{control_type}_line_width''']),
1247
+ position="bottom" ) )
1248
+ self._region_controls['style'][control_type]['line']['alpha'] = Tip( Spinner( width_policy='fixed', width=55, low=0.0, high=1.0, mode='float', step=0.1,
1249
+ value=self._region_style[f'''{pfx}line_alpha'''], margin=(-1, 0, 0, 0) ),
1250
+ tooltip=Tooltip( content=HTML(self._region_style_tips[f'''{control_type}_line_alpha''']),
1251
+ position="bottom" ) )
1252
+ self._region_controls['style'][control_type]['line']['dash'] = Tip( set_attributes( Dropdown( label=self._region_style[f'''{pfx}line_dash'''], button_type='light',
1253
+ margin=(-1, 0, 0, -1),
1254
+ width=130, menu=_dashes ), **kw ),
1255
+ tooltip=Tooltip( content=HTML(self._region_style_tips[f'''{control_type}_line_dash''']), position="bottom" ) )
1256
+ return True
1257
+
1258
+ else:
1259
+ self._region_controls['style'][control_type]['fill']['color'] = reuse[control_type]['fill'][0]
1260
+ self._region_controls['style'][control_type]['fill']['alpha'] = reuse[control_type]['fill'][1]
1261
+ self._region_controls['style'][control_type]['fill']['hatch'] = reuse[control_type]['fill'][2]
1262
+ self._region_controls['style'][control_type]['line']['color'] = reuse[control_type]['line'][0]
1263
+ self._region_controls['style'][control_type]['line']['width'] = reuse[control_type]['line'][1]
1264
+ self._region_controls['style'][control_type]['line']['alpha'] = reuse[control_type]['line'][2]
1265
+ self._region_controls['style'][control_type]['line']['dash'] = reuse[control_type]['line'][3]
1266
+ return False
1267
+
1268
+ if 'style' not in self._region_controls:
1269
+
1270
+ self._region_controls['style'] = { }
1271
+
1272
+ if self._bitmask is not None:
1273
+ raise RuntimeError('CubeMask: region selection not in use')
1274
+
1275
+ if len(self._annotations) == 0:
1276
+ raise RuntimeError('CubeMask: cannot fetch region controls before annotation creation')
1277
+
1278
+ create_region_control( 'default' )
1279
+ create_region_control( 'hover' )
1280
+ create_region_control( 'selected' )
1281
+
1282
+ for k1, v1 in self._region_controls['style']['selected'].items( ):
1283
+ for k2, v2 in v1.items( ):
1284
+ v2.child.disabled = True
1285
+
1286
+ ###
1287
+ ### Changes to the hover styling are immediately applied to all annotations
1288
+ ###
1289
+ alpha_code = '''casalib.map( (v) => { v[attr] = cb_obj.value }, source._annos )'''
1290
+ color_code = '''casalib.map( (v) => v[attr] = cb_obj.color, source._annos )'''
1291
+ hatch_code = '''const name = this.item.replaceAll(' ','_')
1292
+ casalib.map( (v) => v[attr] = name, source._annos )
1293
+ this.origin.label = this.item'''
1294
+
1295
+ self._region_controls['style']['hover']['fill']['hatch'].child.js_on_click( CustomJS( args=dict( values=_hatches, labels=[h.replace('_',' ') for h in _hatches],
1296
+ attr='hover_hatch_pattern',
1297
+ source=self._image_source,
1298
+ annotations=self._annotations ),
1299
+ code=hatch_code ) )
1300
+ self._region_controls['style']['hover']['fill']['alpha'].child.js_on_change( 'value',
1301
+ CustomJS( args=dict( source=self._image_source,
1302
+ attr='hover_fill_alpha' ),
1303
+ code=alpha_code ) )
1304
+ self._region_controls['style']['hover']['fill']['color'].child.js_on_change( 'color', CustomJS( args=dict( source=self._image_source,
1305
+ attr='hover_fill_color' ),
1306
+ code=color_code ) )
1307
+ self._region_controls['style']['hover']['line']['dash'].child.js_on_click( CustomJS( args=dict( attr='hover_line_dash',
1308
+ source=self._image_source ),
1309
+ code=hatch_code ) )
1310
+ self._region_controls['style']['hover']['line']['alpha'].child.js_on_change( 'value',
1311
+ CustomJS( args=dict( source=self._image_source,
1312
+ attr='hover_line_alpha' ),
1313
+ code=alpha_code ) )
1314
+ self._region_controls['style']['hover']['line']['width'].child.js_on_change( 'value',
1315
+ CustomJS( args=dict( source=self._image_source,
1316
+ attr='hover_line_width'),
1317
+ code=alpha_code ) )
1318
+ self._region_controls['style']['hover']['line']['color'].child.js_on_change( 'color', CustomJS( args=dict( source=self._image_source,
1319
+ attr='hover_line_color' ),
1320
+ code=color_code ) )
1321
+
1322
+ return dict( hover=dict( fill=( self._region_controls['style']['hover']['fill']['color'],
1323
+ self._region_controls['style']['hover']['fill']['alpha'],
1324
+ self._region_controls['style']['hover']['fill']['hatch'] ),
1325
+ line=( self._region_controls['style']['hover']['line']['color'],
1326
+ self._region_controls['style']['hover']['line']['width'],
1327
+ self._region_controls['style']['hover']['line']['alpha'],
1328
+ self._region_controls['style']['hover']['line']['dash'] ) ),
1329
+ default=dict( fill=( self._region_controls['style']['default']['fill']['color'],
1330
+ self._region_controls['style']['default']['fill']['alpha'],
1331
+ self._region_controls['style']['default']['fill']['hatch'] ),
1332
+ line=( self._region_controls['style']['default']['line']['color'],
1333
+ self._region_controls['style']['default']['line']['width'],
1334
+ self._region_controls['style']['default']['line']['alpha'],
1335
+ self._region_controls['style']['default']['line']['dash'] ) ),
1336
+ selected=dict( fill=( self._region_controls['style']['selected']['fill']['color'],
1337
+ self._region_controls['style']['selected']['fill']['alpha'],
1338
+ self._region_controls['style']['selected']['fill']['hatch'] ),
1339
+ line=( self._region_controls['style']['selected']['line']['color'],
1340
+ self._region_controls['style']['selected']['line']['width'],
1341
+ self._region_controls['style']['selected']['line']['alpha'],
1342
+ self._region_controls['style']['selected']['line']['dash'] ) ) )
1343
+
1344
+ def channel_ctrl( self ):
1345
+ '''Return a text label for the current channel being displayed.
1346
+ It will be updated as the channel or stokes axis changes.
1347
+ '''
1348
+ if self._image is None:
1349
+ raise RuntimeError('CubeMask: image must be retrieved with image(...) before calling this function')
1350
+ self._channel_ctrl = PreText( text='Channel 0', min_width=100 )
1351
+ self._channel_ctrl_stokes_dropdown = Dropdown( label='I', button_type='light', margin=(-1, 0, 0, 0), sizing_mode='scale_height', width=25 )
1352
+ self._channel_ctrl_group = ( self._channel_ctrl,
1353
+ Tip( self._channel_ctrl_stokes_dropdown,
1354
+ tooltip=Tooltip( content=HTML('Select which of the <b>image</b> or <b>stokes</b> planes to display'),
1355
+ position='right' ) ) )
1356
+ return self._channel_ctrl_group
1357
+
1358
+ def coord_ctrl( self ):
1359
+ '''Return a text label for the current channel being displayed.
1360
+ It will be updated as the channel or stokes axis changes.
1361
+ '''
1362
+ if self._image is None:
1363
+ raise RuntimeError('cube image not in use')
1364
+ self._coord_ctrl_dropdown = Dropdown( label='world', button_type='light', margin=(-1, 0, 0, 0),
1365
+ sizing_mode='scale_height', menu=['pixel','world'] )
1366
+ self._coord_ctrl_dropdown.js_on_click( CustomJS( args=dict( source=self._image_source,
1367
+ xaxis=self._image.xaxis.formatter,
1368
+ yaxis=self._image.yaxis.formatter ),
1369
+ code='''xaxis.coordinates(this.item)
1370
+ yaxis.coordinates(this.item)
1371
+ source.signal_change( )
1372
+ this.origin.label = this.item''' ) )
1373
+
1374
+ self._coord_ctrl_group = Tip( self._coord_ctrl_dropdown,
1375
+ tooltip=Tooltip( content=HTML("Axes can be labeled in <b>pixel</b> or <b>world</b> coordinates"),
1376
+ position="right" ) )
1377
+
1378
+ return self._coord_ctrl_group
1379
+
1380
+ def status_text( self, text='', reuse=None, **kw ):
1381
+ if reuse is None:
1382
+ self._status_div = set_attributes( Div( text=text ), **kw )
1383
+ else:
1384
+ self._status_div = reuse
1385
+ return self._status_div
1386
+
1387
+ def pixel_tracking_text( self, **kw ):
1388
+
1389
+ self._pixel_tracking_text = Div( text='', min_width=200, **kw )
1390
+
1391
+ async def fetch_spectrum( msg, self=self ):
1392
+ if msg['action'] == 'spectrum':
1393
+ chan = msg['value']['chan']
1394
+ index = msg['value']['index']
1395
+ spectrum, mask_value = self._pipe['image'].spectrum( index + [chan[0]], True )
1396
+ mask = { } if mask_value is None else dict( mask=mask_value )
1397
+ return dict( result='success', update=dict( spectrum=spectrum,
1398
+ index=index, chan=chan,
1399
+ **mask ) )
1400
+
1401
+ self._pipe['control'].register( self._ids['fetch-spectrum'], fetch_spectrum )
1402
+ return self._pixel_tracking_text
1403
+
1404
+ def connect( self ):
1405
+ '''Connect the callbacks which are used by the masking GUIs that
1406
+ have been created.
1407
+ '''
1408
+ def set_source_init_function( ):
1409
+ ########################################################################################################################
1410
+ ### Image source initialization: setup of the init function is here to allow the user to assign it after the ###
1411
+ ### CubeMask is created to allow the initialization function to have a reference to ###
1412
+ ### to all image sources (from multiple CubeMasks) that may be a part of the GUI ###
1413
+ ########################################################################################################################
1414
+ if self._mask_path is None:
1415
+ ###
1416
+ ### Region creation WITHOUT an on-disk bitmap
1417
+ ###
1418
+ init_args=dict( annotations=self._annotations, ctrl=self._pipe['control'], ids=self._ids,
1419
+ id=self._image_source.id,
1420
+ stats_source=self._statistics_source, chan_slider=self._slider,
1421
+ mask_region_button=None,
1422
+ mask_region_icons=None,
1423
+ mask_region_ds=self._bitmask_contour_maskmod_ds,
1424
+ contour_ds=self._bitmask_contour_ds,
1425
+ status=self._status_div, statprec=7,
1426
+ user_init_script = self.init_script,
1427
+ stokes_labels=[ k for k,_ in self._region_controls['coord']['chan'].items( ) ],
1428
+ status_line=self._region_controls['coord']['status'],
1429
+ #default_region_color=self._region_controls['tracking']['color-picker'],
1430
+ #chan_select = [ v for k,v in self._region_controls['coord']['chan'].items( ) ],
1431
+ freeze_cb = self._image_freeze_cb,
1432
+ styling={ 'fill': { 'color': self._region_controls['style']['default']['fill']['color'].child,
1433
+ 'alpha': self._region_controls['style']['default']['fill']['alpha'].child,
1434
+ 'hatch': self._region_controls['style']['default']['fill']['hatch'].child },
1435
+ 'line': { 'color': self._region_controls['style']['default']['line']['color'].child,
1436
+ 'width': self._region_controls['style']['default']['line']['width'].child,
1437
+ 'alpha': self._region_controls['style']['default']['line']['alpha'].child,
1438
+ 'dash': self._region_controls['style']['default']['line']['dash'].child } } )
1439
+ else:
1440
+ ###
1441
+ ### On-disk bitmask creation
1442
+ ###
1443
+ init_args=dict( annotations=self._annotations, ctrl=self._pipe['control'], ids=self._ids,
1444
+ stats_source=self._statistics_source, chan_slider=self._slider,
1445
+ mask_region_button=self._mask_add_sub['mask'],
1446
+ mask_region_icons=self._mask_icons_,
1447
+ mask_region_ds=self._bitmask_contour_maskmod_ds,
1448
+ contour_ds=self._bitmask_contour_ds,
1449
+ status=self._status_div, statprec=7,
1450
+ selector=self._bitmask_color_selector,
1451
+ user_init_script = self.init_script,
1452
+ freeze_cb = self._image_freeze_cb )
1453
+
1454
+ self._image_source.init_script = CustomJS( args=init_args,
1455
+ code='''let source = cb_obj
1456
+ document._cube_already_shutdown = false
1457
+ ''' + self._js['mask-state-init'] +
1458
+ ( self._js['func-curmasks']( ) +
1459
+ self._js['key-state-funcs']
1460
+ if self._mask_path is None else '' ) + self._js['setup-key-mgmt'] +
1461
+ """// This function is called to collect the masks and/or stop
1462
+ // -->> collect_masks( ) is only defined if bitmask cube is NOT used
1463
+ document._cube_done = ( final_polys=null, cb=null ) => {
1464
+ if ( document._cube_already_shutdown ) return
1465
+ function done_close_window( msg ) {
1466
+ if ( msg.result === 'stopped' ) {""" +
1467
+ # Don't close tab if running in a jupyter notebook
1468
+ ("""console.log("Running in jupyter notebook. Not closing window.")""" if self._is_notebook else
1469
+ """console.log("Running from script/terminal. Closing window.")
1470
+ window.close()"""
1471
+ ) +
1472
+ """
1473
+ }
1474
+ }
1475
+ document._cube_already_shutdown = true
1476
+ ctrl.send( ids['done'],
1477
+ { action: 'done',
1478
+ value: { regions: final_polys ? final_polys : source.masks( ) },
1479
+ wtf: 'debugging' },
1480
+ (msg) => { if ( ! cb || cb && cb(msg) ) done_close_window(msg) } )
1481
+ }
1482
+ // exported functions -- enable/disable masking, retrieve masks etc.
1483
+ source._masking_enabled = true
1484
+ source._pixel_update_enabled = true
1485
+ if ( ! ('_masking_state' in document) ) {
1486
+ document._masking_state = { }
1487
+ }
1488
+ document._masking_state[source.id] = false
1489
+ source.masking_on = ( ) => { return document._masking_state[source.id] }
1490
+ source.disable_masking = ( ) => { source._masking_enabled = false; document._masking_state[source.id] = false }
1491
+ source.enable_masking = ( ) => { source._masking_enabled = true; document._masking_state[source.id] = true }
1492
+ source.disable_pixel_update = ( ) => source._pixel_update_enabled = false
1493
+ source.enable_pixel_update = ( ) => source._pixel_update_enabled = true
1494
+ source.masks = ( ) => typeof collect_masks == 'function' ? collect_masks( ) : { masks: [], polys: [] }
1495
+ source.breadcrumbs = ( ) => typeof source._mask_breadcrumbs !== 'undefined' ? source._mask_breadcrumbs : null
1496
+ source.drop_breadcrumb = ( code ) => source._mask_breadcrumbs += code
1497
+ source.update_statistics = ( data ) => {
1498
+ data.values.forEach( (item, index) => {
1499
+ /** round floats **/
1500
+ if ( typeof item == 'number' && ! Number.isInteger(item) ) {
1501
+ data.values[index] = Math.round((item + Number.EPSILON) * 10**statprec) / 10**statprec
1502
+ } } )
1503
+ stats_source.data = data
1504
+ }
1505
+ if ( stats_source ) source.update_statistics( stats_source.data ) /*** round pre-filled floats ***/
1506
+ if ( user_init_script ) { user_init_script.execute(this) }
1507
+ """ )
1508
+
1509
+ def region_position_connections( ):
1510
+
1511
+ if not self._region_controls['coord']['initialized'] or self._mask_path is not None:
1512
+ return None
1513
+
1514
+ set_active = '''function set_active( s ) {
1515
+ const disabled = s == false
1516
+ sx.disabled = disabled
1517
+ sy.disabled = disabled
1518
+ wx.disabled = disabled
1519
+ wy.disabled = disabled
1520
+ label.disabled = disabled
1521
+ style.line.color.disabled = disabled
1522
+ style.line.width.disabled = disabled
1523
+ style.line.dash.disabled = disabled
1524
+ style.line.alpha.disabled = disabled
1525
+ style.fill.alpha.disabled = disabled
1526
+ style.fill.color.disabled = disabled
1527
+ style.fill.hatch.disabled = disabled
1528
+ chan_select.map( (v) => v.disabled = disabled )
1529
+ }'''
1530
+
1531
+ to_world = '''function to_world( spt ) {
1532
+ const pt = new casalib.coordtxl.Point2D( Number(spt[0]), Number(spt[1]) )
1533
+ isource.wcs( ).imageToWorldCoords(pt,false)
1534
+ // >>>>>>>>>>--------------J2000---------------------------------vvvvvv
1535
+ return new casalib.coordtxl.WorldCoords(pt.getX(),pt.getY()).format(2000.0)
1536
+ }'''
1537
+
1538
+ from_world = '''function from_world( spt ) {
1539
+ const world = new casalib.coordtxl.WorldCoords(spt[0],spt[1])
1540
+ const pt = new casalib.coordtxl.Point2D(world.getX(),world.getY())
1541
+ isource.wcs( ).worldToImageCoords(pt,false)
1542
+ return [pt.x,pt.y]
1543
+ }'''
1544
+
1545
+ parse_ranges = '''function parse_ranges(str) {
1546
+ const ranges = [];
1547
+
1548
+ // Split the string into individual ranges
1549
+ const rangeStrings = str.split(',');
1550
+
1551
+ for (const rangeStr of rangeStrings) {
1552
+ // Split each range into start and end
1553
+ const [startStr, endStr] = rangeStr.trim().split('-');
1554
+
1555
+ const start = parseInt(startStr, 10);
1556
+ const end = endStr ? parseInt(endStr, 10) : start;
1557
+
1558
+ if (isNaN(start) || isNaN(end) || start > end) {
1559
+ throw new Error(`Invalid range: ${rangeStr}`);
1560
+ }
1561
+
1562
+ ranges.push({ start, end });
1563
+ }
1564
+
1565
+ return ranges;
1566
+ }'''
1567
+
1568
+ set_coordinates = '''const sxct = casalib.minmax(anno.xs).reduce((a, b) => a + b, 0) / 2.0
1569
+ const syct = casalib.minmax(anno.ys).reduce((a, b) => a + b, 0) / 2.0
1570
+ sx._value = sx.value = sxct.toFixed(5)
1571
+ sy._value = sy.value = syct.toFixed(5)
1572
+ const wct = to_world([sxct,syct])
1573
+ wx._value = wx.value = wct[0]
1574
+ wy._value = wy.value = wct[1]
1575
+ tracker.xs = [ sxct-1, sxct-1, sxct+1, sxct+1 ]
1576
+ tracker.ys = [ syct-1, syct+1, syct+1, syct-1 ]
1577
+ label.value = anno._poly.label; '''
1578
+
1579
+ ###
1580
+ ### As Bokeh annotation is dragged or adjusted, keep polygon state in sync
1581
+ ###
1582
+ update_polygon = '''if ( anno._poly ) {
1583
+ anno._poly.geometry.xs = anno.xs.slice( )
1584
+ anno._poly.geometry.ys = anno.ys.slice( )
1585
+ } else { console.log( 'internal error: could not match annotation to polygon' ) }'''
1586
+
1587
+ def pixel_translate( input, pts ):
1588
+ return to_world + from_world + f'''
1589
+ const anno = tracker._current_region
1590
+ if ( ! tracker._disabled ) tracker.visible = true
1591
+ if ( anno && {input}.value != {input}._value ) {{
1592
+ const translation = {input}.value - {input}._value
1593
+ anno.{pts} = anno.{pts}.map((v) => v + translation)
1594
+ tracker.{pts} = tracker.{pts}.map((v) => v + translation)
1595
+ {input}._value = {input}.value
1596
+ const wct = to_world([Number(sx.value),Number(sy.value)])
1597
+ wx._value = wx.value = wct[0]
1598
+ wy._value = wy.value = wct[1]
1599
+ }}'''
1600
+
1601
+ def world_translate( input, pts, input_screen, input_screen_index ):
1602
+ return from_world + f'''
1603
+ const anno = tracker._current_region
1604
+ if ( ! tracker._disabled ) tracker.visible = true
1605
+ if ( anno && {input}.value != {input}._value ) {{
1606
+ const pixel = from_world( [ wx.value, wy.value ] )
1607
+ const translation = pixel[{input_screen_index}] - {input_screen}.value
1608
+ anno.{pts} = anno.{pts}.map((v) => v + translation)
1609
+ tracker.{pts} = tracker.{pts}.map((v) => v + translation)
1610
+ const center = casalib.minmax(anno.{pts}).reduce((a, b) => a + b, 0) / 2.0
1611
+ {input_screen}.value_ = {input_screen}.value = center.toFixed(5)
1612
+ }}'''
1613
+
1614
+ update_status = '''function update_status( str ) {
1615
+ const msg = `<b style='color:red;'>${str}</b>`
1616
+ status.text = msg
1617
+ setTimeout( ( ) => { if ( status.text == msg ) status.text = '' }, 5000 )
1618
+ }'''
1619
+
1620
+ self._region_controls['tracking']['color-picker'].child.js_on_change( 'color',
1621
+ CustomJS( args=dict( tracker=self._region_controls['tracking']['pointer'] ),
1622
+ code='''tracker.line_color = cb_obj.color
1623
+ tracker.fill_color = cb_obj.color''' ) )
1624
+ self._region_controls['tracking']['enable'].child.js_on_event( 'menu_item_click',
1625
+ CustomJS( args=dict( tracker=self._region_controls['tracking']['pointer'] ),
1626
+ code='''if ( cb_obj.item != cb_obj.origin.label ) {
1627
+ cb_obj.origin.label = cb_obj.item
1628
+ tracker._disabled = cb_obj.item == 'disable'
1629
+ }''' ) )
1630
+ translation_dict = dict( isource=self._image_source,
1631
+ sx = self._region_controls['coord']['sx'].child,
1632
+ sy = self._region_controls['coord']['sy'].child,
1633
+ wx = self._region_controls['coord']['wx'].child,
1634
+ wy = self._region_controls['coord']['wy'].child,
1635
+ label = self._region_controls['coord']['label'].child,
1636
+ tracker=self._region_controls['tracking']['pointer'] )
1637
+
1638
+ self._region_controls['coord']['sx'].child.js_on_event( ValueSubmit,
1639
+ CustomJS( args=translation_dict,
1640
+ code=pixel_translate('sx','xs') ) )
1641
+ self._region_controls['coord']['sy'].child.js_on_event( ValueSubmit,
1642
+ CustomJS( args=translation_dict,
1643
+ code=pixel_translate('sy','ys') ) )
1644
+ self._region_controls['coord']['wx'].child.js_on_event( ValueSubmit,
1645
+ CustomJS( args=translation_dict,
1646
+ code=world_translate('wx','xs','sx','0') ) )
1647
+ self._region_controls['coord']['wy'].child.js_on_event( ValueSubmit,
1648
+ CustomJS( args=translation_dict,
1649
+ code=world_translate('wy','ys','sy','1') ) )
1650
+
1651
+ self._region_controls['coord']['label'].child.js_on_event( ValueSubmit,
1652
+ CustomJS( args=dict( isource=self._image_source,
1653
+ tracker=self._region_controls['tracking']['pointer'],
1654
+ status_line=self._region_controls['coord']['status'] ),
1655
+ code='''const anno = tracker._current_region
1656
+ const newlabel = cb_obj.value.trim( )
1657
+ if ( anno && anno._poly && newlabel.length > 0 ) {
1658
+ if ( /^[a-zA-Z0-9_]*$/.test(newlabel) ) {
1659
+ if ( isource._polys.list( ).some( (p) => {if ( p.label == newlabel ) return true} ) ) {
1660
+ const msg = `sorry, only one poly can be labeled "${newlabel}"`
1661
+ status_line.text = msg
1662
+ setTimeout( ( ) => { if ( status_line.text == msg ) status_line.text = '' }, 5000 )
1663
+ } else {
1664
+ anno._poly.label = newlabel
1665
+ const msg = `label set to "${newlabel}"`
1666
+ status_line.text = msg
1667
+ setTimeout( ( ) => { if ( status_line.text == msg ) status_line.text = '' }, 5000 )
1668
+ }
1669
+ } else {
1670
+ status_line.text = '<p>only alphanumeric or underscore characters allowed</p>'
1671
+ }
1672
+ }''' ) )
1673
+
1674
+ for index, (s,text) in enumerate(self._region_controls['coord']['chan'].items( )):
1675
+ text.child.js_on_event( ValueSubmit,
1676
+ CustomJS( args=dict( tracker=self._region_controls['tracking']['pointer'],
1677
+ text=text, source=self._image_source,
1678
+ status=self._region_controls['coord']['status'],
1679
+ stokes=(index,s) ),
1680
+ code=parse_ranges + update_status +
1681
+ #self._js['func-curmasks']('isource') +
1682
+ '''if ( tracker._current_region ) {
1683
+ status.text=''
1684
+ const ranges = casalib.strparse_intranges(cb_obj.value,true)
1685
+ const minmax = casalib.minmax(ranges.flat(Infinity))
1686
+
1687
+ if ( minmax[0] < 0 ) nupdate_status('negative range')
1688
+ else if ( minmax[1] >= source.num_chans[1] ) update_status('exceeds channel range')
1689
+ else {
1690
+ const chans_as_set = ranges.reduce(
1691
+ (set,v) => casalib.forexpr( v[0], v[1], (s,i) => s.add([stokes[0],i]), set),
1692
+ new EqSet( ) )
1693
+ tracker._current_region._poly.putchans( chans_as_set )
1694
+ cb_obj.origin.value = casalib.intlist_to_rangestr(
1695
+ casalib.reduce( (acc,chan) => {
1696
+ if (chan[0] == stokes[0]) acc.push(chan[1])
1697
+ return acc }, chans_as_set, [ ] ) )
1698
+ }
1699
+ } else update_status('no region selected')''' ) )
1700
+
1701
+ ###
1702
+ ### set _region_controls_newpoly so it can be passed along and eventuall called by
1703
+ ### the newpoly(...) function each time a polygon annotation is created
1704
+ ###
1705
+ self._region_controls_newpoly = CustomJS( args=dict( isource=self._image_source,
1706
+ tracker=self._region_controls['tracking']['pointer'],
1707
+ sx = self._region_controls['coord']['sx'],
1708
+ sy = self._region_controls['coord']['sy'],
1709
+ wx = self._region_controls['coord']['wx'],
1710
+ wy = self._region_controls['coord']['wy'] ),
1711
+ code=to_world + '''const anno = cb_data[0]; ''' + set_coordinates )
1712
+
1713
+ self._image_freeze_cb.append( CustomJS( args=dict( isource=self._image_source,
1714
+ tracker=self._region_controls['tracking']['pointer'] ),
1715
+ code='''if ( ! tracker._disabled && tracker._current_region != null ) {
1716
+ tracker.visible = true
1717
+ }''' ) )
1718
+
1719
+ ###
1720
+ ### This is actually called each time the cursor enters the image area (so the location
1721
+ ### data is cleared when the cursor enters the image, which seems good...)
1722
+ ###
1723
+ self._image_unfreeze_cb.append( CustomJS( args=dict( chan_select=[ v.child for k,v in self._region_controls['coord']['chan'].items( ) ],
1724
+ style={ k1: { k2: v2.child for k2,v2 in v1.items( ) }
1725
+ for k1,v1 in self._region_controls['style']['selected'].items( ) },
1726
+ **translation_dict ),
1727
+ code= set_active +
1728
+ '''set_active(false)
1729
+ tracker._current_region = null
1730
+ tracker.visible = false
1731
+ sx._value = sx.value = ''
1732
+ sy._value = sy.value = ''
1733
+ wx._value = wx.value = ''
1734
+ wy._value = wy.value = ''
1735
+ label.value = ''
1736
+ chan_select.forEach( (t) => t.value='' )''' ) )
1737
+
1738
+ for anno in self._annotations:
1739
+ anno.js_on_event( 'rangesupdate', CustomJS( args=dict( anno=anno, **translation_dict ),
1740
+ code=to_world + set_coordinates +
1741
+ self._js['func-curmasks']('isource') +
1742
+ update_polygon ) )
1743
+ anno.js_on_event( 'panstart', CustomJS( args=dict( anno=anno, **translation_dict ),
1744
+ code=to_world + set_coordinates +
1745
+ '''if ( ! tracker._disabled ) tracker.visible = true''' ) )
1746
+ anno.js_on_event( 'panend', CustomJS( args=dict( isource=self._image_source,
1747
+ tracker=self._region_controls['tracking']['pointer'] ),
1748
+ code='''if ( ! isource._freeze_cursor_update ) tracker.visible = false''' ) )
1749
+ anno.js_on_event( 'mouseenter', CustomJS( args=dict( anno=anno,
1750
+ chan_select=[ v.child for k,v in self._region_controls['coord']['chan'].items( ) ],
1751
+ style={ k1: { k2: v2.child for k2,v2 in v1.items( ) }
1752
+ for k1,v1 in self._region_controls['style']['selected'].items( ) },
1753
+ **translation_dict ),
1754
+ code=to_world + set_active +
1755
+ '''if ( ! isource._freeze_cursor_update && anno._poly ) {
1756
+ tracker._current_region = anno
1757
+ set_active( true )
1758
+ style.fill.alpha.value = anno._poly.styling.fill.alpha
1759
+ style.fill.color.color = anno._poly.styling.fill.color
1760
+ style.fill.hatch.label = anno._poly.styling.fill.hatch.replaceAll('_',' ')
1761
+ style.line.alpha.value = anno._poly.styling.line.alpha
1762
+ style.line.color.color = anno._poly.styling.line.color
1763
+ style.line.dash.label = anno._poly.styling.line.dash
1764
+ style.line.width.value = anno._poly.styling.line.width
1765
+ /*** update channel range ***/
1766
+ chan_select.forEach( (chan_txt, idx) => {
1767
+ chan_txt.value = casalib.intlist_to_rangestr(
1768
+ casalib.reduce( (acc,chan) => {
1769
+ if (chan[0] == idx) acc.push(chan[1])
1770
+ return acc }, anno._poly.getchans( ), [ ] ) )
1771
+ } )
1772
+ ''' + set_coordinates + '''
1773
+ }''' ) )
1774
+
1775
+ def region_style_changes( ):
1776
+ if 'style' in self._region_controls and \
1777
+ 'selected' in self._region_controls['style']:
1778
+ ARGS=dict( source=self._image_source,
1779
+ tracker=self._region_controls['tracking']['pointer'] )
1780
+ def style_code( cb_obj_src, anno_tgt, poly_tgt, extra='' ):
1781
+ return f'''if ( tracker._current_region ) {{
1782
+ const anno = tracker._current_region
1783
+ if ( anno._poly ) {{
1784
+ anno.{anno_tgt} = cb_obj.{cb_obj_src}
1785
+ anno._poly.styling.{poly_tgt} = anno.{anno_tgt}
1786
+ {extra}
1787
+ }}
1788
+ }}'''
1789
+ self._region_controls['style']['selected']['fill']['alpha'].child.js_on_change( 'value',
1790
+ CustomJS( args=ARGS,
1791
+ code=style_code( "value", "fill_alpha", "fill.alpha" ) ) )
1792
+ self._region_controls['style']['selected']['fill']['color'].child.js_on_change( 'color',
1793
+ CustomJS( args=ARGS,
1794
+ code=style_code( "color", "fill_color", "fill.color" ) ) )
1795
+ self._region_controls['style']['selected']['fill']['hatch'].child.js_on_click( CustomJS( args=ARGS,
1796
+ code=style_code( "item.replaceAll(' ','_')", "hatch_pattern", "fill.hatch",
1797
+ '''cb_obj.origin.label = cb_obj.item''' ) ) )
1798
+ self._region_controls['style']['selected']['line']['alpha'].child.js_on_change( 'value',
1799
+ CustomJS( args=ARGS,
1800
+ code=style_code( "value", "line_alpha", "line.alpha" ) ) )
1801
+ self._region_controls['style']['selected']['line']['color'].child.js_on_change( 'color',
1802
+ CustomJS( args=ARGS,
1803
+ code=style_code( "color", "line_color", "line.color" ) ) )
1804
+ self._region_controls['style']['selected']['line']['dash'].child.js_on_click( CustomJS( args=ARGS,
1805
+ code=style_code( "item", "line_dash", "line.dash",
1806
+ '''cb_obj.origin.label = cb_obj.item''' ) ) )
1807
+ self._region_controls['style']['selected']['line']['width'].child.js_on_change( 'value',
1808
+ CustomJS( args=ARGS,
1809
+ code=style_code( "value", "line_width", "line.width" ) ) )
1810
+
1811
+ self.CCOUNT = self.CCOUNT + 1
1812
+
1813
+ set_source_init_function( )
1814
+ region_position_connections( )
1815
+ region_style_changes( )
1816
+
1817
+ if self._mask_path:
1818
+ self._mask_add_sub['add'].callback = CustomJS( args=dict( annotations=self._annotations,
1819
+ source=self._image_source,
1820
+ ctrl=self._pipe['control'],
1821
+ ids=self._ids,
1822
+ stats_source=self._statistics_source,
1823
+ mask_region_icons=self._mask_icons_,
1824
+ mask_region_button=self._mask_add_sub['mask'],
1825
+ mask_region_ds=self._bitmask_contour_maskmod_ds,
1826
+ contour_ds=self._bitmask_contour_ds,
1827
+ status=self._status_div ),
1828
+ code=self._js_mode_code['bitmask-hotkey-setup-add-sub'] +
1829
+ '''if ( cb_obj._mode == 'cube' ) mask_add_cube( )
1830
+ else mask_add_chan( )''' )
1831
+ self._mask_add_sub['sub'].callback = CustomJS( args=dict( annotations=self._annotations,
1832
+ source=self._image_source,
1833
+ ctrl=self._pipe['control'],
1834
+ ids=self._ids,
1835
+ stats_source=self._statistics_source,
1836
+ mask_region_icons=self._mask_icons_,
1837
+ mask_region_button=self._mask_add_sub['mask'],
1838
+ mask_region_ds=self._bitmask_contour_maskmod_ds,
1839
+ contour_ds=self._bitmask_contour_ds,
1840
+ status=self._status_div ),
1841
+ code=self._js_mode_code['bitmask-hotkey-setup-add-sub'] +
1842
+ '''if ( cb_obj._mode == 'cube' ) mask_sub_cube( )
1843
+ else mask_sub_chan( )''' )
1844
+
1845
+ self._mask_add_sub['mask'].callback = CustomJS( args=dict( annotations=self._annotations,
1846
+ contour_ds=self._bitmask_contour_ds,
1847
+ mask_region_ds=self._bitmask_contour_maskmod_ds,
1848
+ region=self._bitmask_contour_maskmod,
1849
+ selector=self._bitmask_color_selector,
1850
+ mask_region_button=self._mask_add_sub['mask'],
1851
+ mask_region_icons=self._mask_icons_,
1852
+ source=self._image_source,
1853
+ status=self._status_div ),
1854
+ code=self._js_mode_code['bitmask-hotkey-setup-add-sub'] +
1855
+ '''if ( mask_region_button.icon == mask_region_icons['on'] ) source._mask.clear( )
1856
+ else source.mask.set( region )''' )
1857
+
1858
+
1859
+ if self._slider:
1860
+ ###
1861
+ ### this code is here instead of in `def slider(...)` because we do not know if
1862
+ ### the user is using statistics until connect( ) is called...
1863
+ ### ... BUT we also need to handle statistics WITHOUT a slider... hmmm....
1864
+ ### ... NEED TO switch statistics updates to use _image_source.cur_chan instead...
1865
+ ### ... ALSO statistics would be based upon the SELECTION SET...
1866
+ ###
1867
+ self._cb['slider'] = CustomJS( args=dict( isource=self._image_source, slider=self._slider,
1868
+ stats_source=self._statistics_source,
1869
+ pixlabel = self._pixel_tracking_text,
1870
+ min=self._cm_adjust['min input'],
1871
+ max=self._cm_adjust['max input'],
1872
+ span1=self._cm_adjust['span one'],
1873
+ span2=self._cm_adjust['span two'],
1874
+ histogram=self._cm_adjust['histogram'],
1875
+ go_to=self._goto_txt,
1876
+ ids=self._ids, ctrl=self._pipe['control'], pix_wrld=self._coord_ctrl_dropdown ),
1877
+ code=self._js['pixel-update-func'] + (self._js['slider_w_stats'] if self._statistics_source else self._js['slider_wo_stats']) )
1878
+
1879
+ self._slider.js_on_change( 'value', self._cb['slider'] )
1880
+
1881
+ if self._goto:
1882
+ self._goto_stokes.js_on_click( CustomJS( args=dict( source=self._image_source,
1883
+ stokes=self._channel_ctrl_stokes_dropdown,
1884
+ goto_stokes=self._goto_stokes ),
1885
+ ### 'stokes.label' is updated after the channel has changed to allow for subsequent
1886
+ ### updates (e.g. convergence plot) to update based upon 'label' after fresh
1887
+ ### convergence data is available...
1888
+ code= self._js['stokes-change'] % ( ' : '.join( map( lambda x: f'''cb_obj.item == '{x[1]}' ? {x[0]}''',
1889
+ zip(range(len(self._stokes_labels)),self._stokes_labels) ) ) + ' : 0' ) ) )
1890
+ self._goto_txt.js_on_event( 'mouseenter', CustomJS( args=dict( slider=self._slider, dropdown=self._goto_stokes ),
1891
+ code='''cb_obj.origin._has_focus = true
1892
+ const view = Bokeh.find.view(cb_obj.origin)
1893
+ view.input_el.focus( )
1894
+ cb_obj.origin.value = ''
1895
+ dropdown.label = "Go To"''' ) )
1896
+ self._goto_txt.js_on_event( 'mouseleave', CustomJS( args=dict( slider=self._slider, dropdown=self._goto_stokes,
1897
+ stokes=self._channel_ctrl_stokes_dropdown ),
1898
+ code='''const view = Bokeh.find.view(slider)
1899
+ dropdown.label = `${stokes.label} Channel`
1900
+ document.activeElement.blur( )
1901
+ if ( slider ) cb_obj.origin.value = String(slider.value)
1902
+ cb_obj.origin._has_focus = false''' ) )
1903
+
1904
+ self._goto_txt.js_on_event( ValueSubmit, CustomJS( args=dict( img=self._image_source,
1905
+ slider=self._slider,
1906
+ status=self._status_div ),
1907
+ code='''let values = cb_obj.value.split(/[ ,]+/).map((v,) => parseInt(v))
1908
+ if ( values.length > 2 ) {
1909
+ status._error_set = true
1910
+ status.text = '<p>enter at most two indexes</p>'
1911
+ } else if ( values.filter((x) => x < 0 || isNaN(x)).length > 0 ) {
1912
+ status._error_set = true
1913
+ status.text = '<p>invalid channel entered</p>'
1914
+ } else {
1915
+ if ( status._error_set ) {
1916
+ status._error_set = false
1917
+ status.text = '<p/>'
1918
+ }
1919
+ if ( values.length == 1 ) {
1920
+ if ( values[0] >= 0 && values[0] < img.num_chans[1] ) {
1921
+ status._error_set = false
1922
+ status.text= `<p>moving to channel ${values[0]}</p>`
1923
+ slider.value = values[0]
1924
+ } else {
1925
+ status._error_set = true
1926
+ status.text = `<p>channel ${values[0]} out of range</p>`
1927
+ }
1928
+ } else if ( values.length == 2 ) {
1929
+ if ( values[0] < 0 || values[0] >= img.num_chans[1] ) {
1930
+ status._error_set = true
1931
+ status.text = `<p>channel ${values[0]} out of range</p>`
1932
+ } else {
1933
+ if ( values[1] < 0 || values[1] >= img.num_chans[0] ) {
1934
+ status._error_set = true
1935
+ status.text = `<p>stokes ${values[1]} out of range</p>`
1936
+ } else {
1937
+ status._error_set = false
1938
+ status.text= `<p>moving to channel ${values[0]}/${values[1]}</p>`
1939
+ slider.value = values[0]
1940
+ img.channel( values[0], values[1] )
1941
+ }
1942
+ }
1943
+ }
1944
+ }
1945
+ if ( ! status._error_set ) cb_obj.origin.value = "" ''' ) )
1946
+
1947
+
1948
+ if self._statistics_mask:
1949
+ self._statistics_mask.js_on_click( CustomJS( args=dict( source=self._image_source,
1950
+ stats_source=self._statistics_source,
1951
+ ids=self._ids,
1952
+ ctrl=self._pipe['control'] ),
1953
+ ###
1954
+ ### (1) send message to configure statistics behavior
1955
+ ### (2) when reply is received change label and refresh channel display
1956
+ ### (3) when reply is received update statistics
1957
+ ###
1958
+ code='''if ( cb_obj.item != cb_obj.origin.label ) {
1959
+ // >>>>---moving-to-mask-from-channel-----vvvvvvvvvvvvvvvvvvvv
1960
+ const masking_on = cb_obj.origin.label == 'Channel Statistics'
1961
+ ctrl.send( ids['config-statistics'],
1962
+ { action: 'use mask', value: masking_on },
1963
+ (msg) => { cb_obj.origin.label = cb_obj.item
1964
+ source.channel( source.cur_chan[1], source.cur_chan[0],
1965
+ msg => { if ( 'stats' in msg ) { source.update_statistics( msg.stats ) } } ) } ) }
1966
+ ''' ) )
1967
+
1968
+ self._image.js_on_event( MouseEnter, CustomJS( args=dict( source=self._image_source,
1969
+ stats_source=self._statistics_source ),
1970
+ code= ( self._js['func-curmasks']( ) + self._js['key-state-funcs']
1971
+ if self._mask_path is None else "" ) +
1972
+ '''casalib.hotkeys.setScope(source._hotkeys.id)''' ) )
1973
+ self._image.js_on_event( MouseLeave, CustomJS( args=dict( source=self._image_source,
1974
+ stats_source=self._statistics_source ),
1975
+ code= ( self._js['func-curmasks']( ) + self._js['key-state-funcs']
1976
+ if self._mask_path is None else "" ) +
1977
+ '''casalib.hotkeys.setScope( )''' ) )
1978
+
1979
+ self._image_source.js_on_change( 'cur_chan', CustomJS( args=dict( slider=self._slider, label=self._channel_ctrl,
1980
+ stokes_label=self._channel_ctrl_stokes_dropdown,
1981
+ cb=self._channel_callback ),
1982
+ ### the label manipulation portion of 'code' is '' when self._channel_ctrl is None
1983
+ ### so stokes_label.label and label.text will not be updated when they are not used
1984
+ code=( ( '''label.text = `Channel ${cb_obj.cur_chan[1]}`
1985
+ stokes_label.label = ( %s );''' %
1986
+ ( ' : '.join(map( lambda p: f'''cb_obj.cur_chan[0] == {p[0]} ? '{p[1]}' ''',
1987
+ zip( range(len(self._stokes_labels)), self._stokes_labels )) ) + " : ''" ) if
1988
+ self._channel_ctrl else '' ) +
1989
+ ( ( '''if ( casalib.hotkeys.getScope( ) === cb_obj._hotkeys.id ) slider.value = cb_obj.cur_chan[1]''' if
1990
+ self._slider else '') +
1991
+ (self._js['refresh-regions'])
1992
+ if self._mask_path is None else '' ) + ''';if ( cb ) cb.execute( cb_obj )''' ) ) )
1993
+
1994
+ if self._channel_ctrl:
1995
+ ###
1996
+ ### allow switching to stokes planes
1997
+ ###
1998
+ self._channel_ctrl_stokes_dropdown.menu = self._stokes_labels
1999
+ self._channel_ctrl_stokes_dropdown.js_on_click( CustomJS( args=dict( source=self._image_source,
2000
+ stokes=self._channel_ctrl_stokes_dropdown,
2001
+ goto_stokes=self._goto_stokes ),
2002
+ ### 'label' is updated after the channel has changed to allow for subsequent
2003
+ ### updates (e.g. convergence plot) to update based upon 'label' after fresh
2004
+ ### convergence data is available...
2005
+ code= self._js['stokes-change'] % ( ' : '.join( map( lambda x: f'''cb_obj.item == '{x[1]}' ? {x[0]}''',
2006
+ zip(range(len(self._stokes_labels)),self._stokes_labels) ) ) + ' : 0' ) ) )
2007
+
2008
+ ###
2009
+ ### cursor movement code snippets
2010
+ movement_code_spectrum_update = ''
2011
+ movement_code_pixel_update = ''
2012
+ if self._spectrum:
2013
+ ###
2014
+ ### this is set up in connect( ) because slider must be updated if it is used othersize
2015
+ ### channel should be directly set (previously the slider was implicitly set when a new
2016
+ ### channel was selected, but I think this update was broken when the oscillation problem
2017
+ ### we fixed, see above)
2018
+ ###
2019
+ self._cb['sptap'] = CustomJS( args=dict( span=self._sp_span, source=self._image_source,
2020
+ slider=self._slider, specfig=self._spectrum ),
2021
+ code = '''if ( span.location >= 0 && ! specfig?.disabled ) {
2022
+ if ( slider ) slider.value = span.location
2023
+ else source.channel( span.location, source.cur_chan[0] )
2024
+ // chan----^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^-----stokes
2025
+ }''' )
2026
+
2027
+ self._spectrum.js_on_event('tap', self._cb['sptap'])
2028
+
2029
+ ###
2030
+ ### code for spectrum update due to cursor movement
2031
+ ###
2032
+ movement_code_spectrum_update = """if ( isource._freeze_cursor_update == false ) {
2033
+ var x_pos = Math.floor(cb_obj.x)
2034
+ var y_pos = Math.floor(cb_obj.y)
2035
+ if ( isFinite(x_pos) && isFinite(y_pos) && x_pos >= 0 && y_pos >= 0 ) {
2036
+ isource._current_pos = [ x_pos, y_pos ]
2037
+ if ( specfig && ! specfig.disabled ) {
2038
+ /* SEGV: cannot fetch pixels while tclean may be modifying the image */
2039
+ update_spectrum( isource.cur_chan, [ x_pos, y_pos ],
2040
+ ( spec ) => specds.data = spec.spectrum )
2041
+ }
2042
+ }
2043
+ }"""
2044
+
2045
+ if self._pixel_tracking_text:
2046
+ ###
2047
+ ### code for updating pixel value due to cursor movements
2048
+ ###
2049
+ movement_code_pixel_update = '''if ( isource._freeze_cursor_update == false ) {
2050
+ var x_pos = Math.floor(cb_obj.x)
2051
+ var y_pos = Math.floor(cb_obj.y)
2052
+ if ( isFinite(x_pos) && isFinite(y_pos) && x_pos >= 0 && y_pos >= 0 ) {
2053
+ isource._current_pos = [ x_pos, y_pos ]
2054
+ if ( specfig && ! specfig.disabled ) {
2055
+ /* SEGV: cannot fetch pixels while tclean may be modifying the image */
2056
+ update_spectrum( isource.cur_chan, [ x_pos, y_pos ],
2057
+ ( spec ) => {
2058
+ refresh_pixel_display( spec.index,
2059
+ spec.spectrum.pixel[spec.chan[1]],
2060
+ 'mask' in spec ? spec.mask[spec.chan[1]] : undefined,
2061
+ pix_wrld && pix_wrld.label == 'pixel' ? false : true )
2062
+ } )
2063
+ } else if ( isource._pixel_update_enabled ) {
2064
+ /* no spectrum to update */
2065
+ ctrl.send( ids['fetch-spectrum'],
2066
+ { action: 'spectrum',
2067
+ value: { chan: isource.cur_chan,
2068
+ index: isource._current_pos } },
2069
+ ( msg ) => {
2070
+ const spec = msg.update
2071
+ refresh_pixel_display( spec.index,
2072
+ spec.spectrum.pixel[spec.chan[1]],
2073
+ 'mask' in spec ? spec.mask[spec.chan[1]] : undefined,
2074
+ pix_wrld && pix_wrld.label == 'pixel' ? false : true ) }, true )
2075
+ }
2076
+ }
2077
+ }'''
2078
+
2079
+ if movement_code_spectrum_update or movement_code_pixel_update:
2080
+ self._image.js_on_event( 'mousemove', CustomJS( args=dict( specds=self._image_spectrum, specfig=self._spectrum,
2081
+ isource=self._image_source, ids=self._ids, ctrl=self._pipe['control'],
2082
+ pixlabel = self._pixel_tracking_text, pix_wrld=self._coord_ctrl_dropdown,
2083
+ source=self._image_source ),
2084
+ code=self._js['pixel-update-func'] +
2085
+ movement_code_pixel_update +
2086
+ movement_code_spectrum_update ) )
2087
+
2088
+ self._image.js_on_event( 'mouseenter', CustomJS( args=dict( isource=self._image_source,
2089
+ cb=self._image_unfreeze_cb ),
2090
+ code='''isource._freeze_cursor_update = false
2091
+ cb.map( (e) => e.execute( this, e ) )''' ) )
2092
+
2093
+ ###
2094
+ ### This setup is delayed until connect( ) to allow for the use of
2095
+ ### self._bitmask_color_selector
2096
+ ###
2097
+ if self._bitmask_color_selector:
2098
+ self._bitmask_color_selector.js_on_change( 'color', CustomJS( args=dict( source=self._image_source,
2099
+ bitmask=self._bitmask,
2100
+ contour=self._bitmask_contour,
2101
+ region=self._bitmask_contour_maskmod,
2102
+ bitrep=self._bitmask_transparency_button,
2103
+ annotations=self._annotations ),
2104
+ code= ( "" if self._mask_path is None else
2105
+ '''source._mask.get( ).line_color = cb_obj.color;''' ) +
2106
+ '''let cm = bitmask.glyph.color_mapper
2107
+ if ( bitmask._transparent == null ) {
2108
+ bitmask._transparent = cm.palette[0]
2109
+ }
2110
+ if ( bitrep.label == 'masked' ) {
2111
+ cm.palette[1] = cb_obj.color
2112
+ } else if ( bitrep.label == 'unmasked' ) {
2113
+ cm.palette[0] = cb_obj.color
2114
+ }
2115
+ cm.change.emit( )
2116
+ contour.glyph.line_color = cb_obj.color
2117
+ region.glyph.line_color = cb_obj.color''' ) )
2118
+
2119
+ self._image.js_on_event( SelectionGeometry,
2120
+ CustomJS( args=dict( source=self._image_source,
2121
+ annotations=self._annotations,
2122
+ selector=self._bitmask_color_selector,
2123
+ mask_region_button=self._mask_add_sub['mask'] if self._mask_path else None,
2124
+ mask_region_icons=self._mask_icons_ if self._mask_path else None,
2125
+ contour_ds=self._bitmask_contour_ds,
2126
+ mask_region_ds=self._bitmask_contour_maskmod_ds,
2127
+ region_newpoly = self._region_controls_newpoly,
2128
+ stokes_labels=[ k for k,_ in self._region_controls['coord']['chan'].items( ) ],
2129
+ status_line=self._region_controls['coord']['status'] if self._mask_path is None else None,
2130
+ chan_select = [ v.child for k,v in self._region_controls['coord']['chan'].items( ) ] ),
2131
+ code= ( self._js['func-newpoly'] + self._js['func-curmasks']( ) +
2132
+ self._js['mask-state-init'] + self._js_mode_code['no-bitmask-tool-selection'] )
2133
+ if self._mask_path is None else "" + (
2134
+ ### selector indicates if a on-disk mask is being used
2135
+ ''';if ( source.masking_on( ) ) {
2136
+ const geometry = cb_obj['geometry']
2137
+ if ( geometry.type === 'rect' ) {
2138
+ // rectangle drawing complete
2139
+ source._mask.set( [ geometry.x0, geometry.x0, geometry.x1, geometry.x1 ],
2140
+ [ geometry.y0, geometry.y1, geometry.y1, geometry.y0 ] )
2141
+ } else if ( geometry.type === 'poly' && cb_obj.final ) {
2142
+ // polygon drawing complete
2143
+ source._mask.set( [ ].slice.call(geometry.x),
2144
+ [ ].slice.call(geometry.y) )
2145
+ }
2146
+ }''' ) ) )
2147
+
2148
+ ###
2149
+ ### when the user types enter (e.g. within one of the TextEdit widgets)...
2150
+ ### a SelectionGeometry is generated with a zero vertex polygon
2151
+ ### this is complicated by the fact that when the TextInput are reused the
2152
+ ### referenced 'source' that is associated with the TextInput is out
2153
+ ### of sync with the image. Because of this, the state for
2154
+ ### source.enable_masking( )/source.disable_masking( ) is indexed
2155
+ ### off of 'document' based on the 'source.id' when the
2156
+ ### enable/disable functions are created
2157
+ ###
2158
+ self._image.js_on_event( MouseEnter, CustomJS( args=dict( source=self._image_source ),
2159
+ code='''source.enable_masking( )''' ) )
2160
+ self._image.js_on_event( MouseLeave, CustomJS( args=dict( source=self._image_source ),
2161
+ code='''source.disable_masking( )''' ) )
2162
+
2163
+
2164
+
2165
+ def js_obj( self ):
2166
+ '''return the javascript object that can be used for control. This
2167
+ object should contain a ``done`` function which will cause the
2168
+ masking GUI to exit and return the masks that have been drawn
2169
+ Also provides JavaScript functions:
2170
+ disable_masking( ) - disable mask drawing (e.g. interactive clean)
2171
+ enable_masking( ) - enable mask drawing
2172
+ disable_pixel_update( ) - disable pixel text updates, which requires
2173
+ fetching the spectrum (e.g. interactive clean)
2174
+ enable_pixel_update( ) - enable pixel text updates
2175
+ breadcrumbs( ) - fetch breadcrumb trail (MAYBE used to
2176
+ determine when the mask state has changed)
2177
+ drop_breadcrumb( code ) - add string to the breadcrumb trail
2178
+ '''
2179
+ self._init_image_source( )
2180
+ return self._image_source
2181
+
2182
+ def result( self ):
2183
+ '''Retrieve the masks that have been drawn by the user. The return
2184
+ value is a dictionary with two elements ``masks`` and ``polys``.
2185
+
2186
+ The value of the ``masks`` element is a dictionary that is indexed by
2187
+ tuples of ``(stokes,chan)`` and the value of each element is a list
2188
+ whose elements describe the polygons drawn on the channel represented
2189
+ by ``(stokes,chan)``. Each polygon description in this list has a
2190
+ polygon index (``p``) and a x/y translation (``d``).
2191
+
2192
+ The value of the ``polys`` element is a dictionary that is indexed by
2193
+ polygon indexes. The value of each polygon index is a dictionary containing
2194
+ ``type`` (whose value is either ``'rect'`` or ``'poly``) and ``geometry``
2195
+ (whose value is a dictionary containing ``'xs'`` and ``'ys'`` (which are
2196
+ the x and y coordinates that define the polygon).
2197
+ '''
2198
+ return self._result
2199
+
2200
+ def help( self, rows=[ ], **kw ):
2201
+ '''Retrieve the help Bokeh object. When this button is clicked, a tab/window
2202
+ containing the help information is opened or receives focus.
2203
+ '''
2204
+ tip_parameters = ['position']
2205
+ kw_args = { 'position': 'bottom', **kw }
2206
+ if self._help_button is None:
2207
+ self._help_button = set_attributes( TipButton( label="", max_width=35, max_height=35, name='help',
2208
+ button_type='light', hover_wait=0, margin=(-1, 0, 0, 0),
2209
+ tooltip=Tooltip( content=HTML( '''<b>Click</b> here for image command key help.
2210
+ <b>Hover</b> over some widgets for 1.5 seconds for other help''' ),
2211
+ **{ k: v for k,v in kw_args.items( ) if k in tip_parameters} ) ),
2212
+ **{ k: v for k,v in kw_args.items( ) if k not in tip_parameters} )
2213
+ self._help_button.js_on_click( CustomJS( args=dict( text=self.__help_string( ) ),
2214
+ code='''if ( window._iclean_help && ! window._iclean_help.closed ) {
2215
+ window._iclean_help.focus( )
2216
+ } else {
2217
+ window._iclean_help = window.open("about:blank","Interactive Clean Help")
2218
+ window._iclean_help.document.write(text)
2219
+ window._iclean_help.document.close( )
2220
+ }''' ) )
2221
+ return self._help_button
2222
+
2223
+ @asynccontextmanager
2224
+ async def serve( self, stop_function ):
2225
+ self._stop_serving_function = stop_function
2226
+ async with websockets.serve( self._pipe['image'].process_messages, self._pipe['image'].address[0], self._pipe['image'].address[1] ) as im, \
2227
+ websockets.serve( self._pipe['control'].process_messages, self._pipe['control'].address[0], self._pipe['control'].address[1] ) as ctrl:
2228
+ yield { 'im': im, 'ctrl': ctrl }
2229
+ #pass
2230
+
2231
+ def __init_js( self ):
2232
+ ###
2233
+ ### Only initialize once...
2234
+ ###
2235
+ if 'id' in self._hotkey_state:
2236
+ return
2237
+
2238
+ ###
2239
+ ### manage multiple CubeMask objects loaded into browser at once
2240
+ ###
2241
+ self._hotkey_state['id'] = str(uuid4())
2242
+
2243
+ ###########################################################################################################################
2244
+ ### Notes on States ###
2245
+ ### ###
2246
+ ### Global state is tied to the ImageDataSource ###
2247
+ ### ###
2248
+ ### selection buffer: tied to per-channel state ###
2249
+ ### copy buffer: global ###
2250
+ ###########################################################################################################################
2251
+ self._js_mode_code = {
2252
+ 'bitmask-hotkey-setup-add-sub': '''
2253
+ function mask_mod_result( msg ) {
2254
+ if ( msg.result == 'success' ) {
2255
+ if ( 'update' in msg && 'clear_region' in msg.update && msg.update.clear_region ) {
2256
+ /* if the src mask on disk has changed the maskmod region is no longer valid */
2257
+ source._mask.clear( )
2258
+ }
2259
+ source.refresh( msg => { if ( 'stats' in msg ) { source.update_statistics( msg.stats ) } } )
2260
+ }
2261
+ }
2262
+ function mask_add_chan( ) {
2263
+ const anno = source._mask.get( )
2264
+ if ( anno.xs.length > 0 && anno.ys.length > 0 ) {
2265
+ ctrl.send( ids['mask-mod'],
2266
+ { scope: 'chan',
2267
+ action: 'addition',
2268
+ value: { chan: source.cur_chan,
2269
+ xs: anno.xs,
2270
+ ys: anno.ys } },
2271
+ mask_mod_result )
2272
+ } else if ( ! casalib.is_empty(mask_region_ds.data.xs) && ! casalib.is_empty(mask_region_ds.data.ys) ) {
2273
+ ctrl.send( ids['mask-mod'],
2274
+ { scope: 'chan',
2275
+ action: 'addition',
2276
+ value: { chan: source.cur_chan,
2277
+ src: mask_region_ds._src_chan } },
2278
+ mask_mod_result )
2279
+ } else if ( status ) status.text = '<p>no region found</p>'
2280
+ }
2281
+ function mask_sub_chan( ) {
2282
+ if ( annotations[0].xs.length > 0 && annotations[0].ys.length > 0 ) {
2283
+ ctrl.send( ids['mask-mod'],
2284
+ { scope: 'chan',
2285
+ action: 'subtract',
2286
+ value: { chan: source.cur_chan,
2287
+ xs: annotations[0].xs,
2288
+ ys: annotations[0].ys } },
2289
+ mask_mod_result )
2290
+ } else if ( ! casalib.is_empty(mask_region_ds.data.xs) && ! casalib.is_empty(mask_region_ds.data.ys.length) ) {
2291
+ ctrl.send( ids['mask-mod'],
2292
+ { scope: 'chan',
2293
+ action: 'subtract',
2294
+ value: { chan: source.cur_chan,
2295
+ src: mask_region_ds._src_chan } },
2296
+ mask_mod_result )
2297
+ } else if ( status ) status.text = '<p>no region found</p>'
2298
+ }
2299
+ function mask_add_cube( ) {
2300
+ if ( annotations[0].xs.length > 0 && annotations[0].ys.length > 0 ) {
2301
+ ctrl.send( ids['mask-mod'],
2302
+ { scope: 'cube',
2303
+ action: 'addition',
2304
+ value: { chan: source.cur_chan,
2305
+ xs: annotations[0].xs,
2306
+ ys: annotations[0].ys } },
2307
+ mask_mod_result )
2308
+ } else if ( ! casalib.is_empty(mask_region_ds.data.xs) && ! casalib.is_empty(mask_region_ds.data.ys) ) {
2309
+ ctrl.send( ids['mask-mod'],
2310
+ { scope: 'cube',
2311
+ action: 'addition',
2312
+ value: { chan: source.cur_chan,
2313
+ src: mask_region_ds._src_chan } },
2314
+ mask_mod_result )
2315
+ } else if ( status ) status.text = '<p>no region found</p>'
2316
+ }
2317
+ function mask_sub_cube( ) {
2318
+ if ( annotations[0].xs.length > 0 && annotations[0].ys.length > 0 ) {
2319
+ ctrl.send( ids['mask-mod'],
2320
+ { scope: 'cube',
2321
+ action: 'subtract',
2322
+ value: { chan: source.cur_chan,
2323
+ xs: annotations[0].xs,
2324
+ ys: annotations[0].ys } },
2325
+ mask_mod_result )
2326
+ } else if ( ! casalib.is_empty(mask_region_ds.data.xs) && ! casalib.is_empty(mask_region_ds.data.ys) ) {
2327
+ ctrl.send( ids['mask-mod'],
2328
+ { scope: 'cube',
2329
+ action: 'subtract',
2330
+ value: { chan: source.cur_chan,
2331
+ src: mask_region_ds._src_chan } },
2332
+ mask_mod_result )
2333
+ } else if ( status ) status.text = '<p>no region found</p>'
2334
+ }''',
2335
+ 'bitmask-hotkey-setup': '''
2336
+ function state_translate_selection( dx, dy ) {
2337
+ const shape = source.image_source.shape
2338
+ // regions can move out of image, later outlier images may be included
2339
+ if ( dx !== 0 ) annotations[0].xs = annotations[0].xs.map( x => x + dx )
2340
+ if ( dy !== 0 ) annotations[0].ys = annotations[0].ys.map( y => y + dy )
2341
+ }
2342
+ casalib.hotkeys( 'escape', { scope: source._hotkeys.id },
2343
+ (e) => { e.preventDefault( )
2344
+ source._mask.clear( ) } )
2345
+ casalib.hotkeys( 'f', { scope: source._hotkeys.id },
2346
+ (e) => { source._freeze_cursor_update = true
2347
+ freeze_cb.map( (e) => e.execute( this, e ) ) } )
2348
+ casalib.hotkeys( 'a', { scope: source._hotkeys.id },
2349
+ (e) => mask_add_chan( ) )
2350
+ casalib.hotkeys( 's', { scope: source._hotkeys.id },
2351
+ (e) => mask_sub_chan( ) )
2352
+ casalib.hotkeys( 'shift+a', { scope: source._hotkeys.id },
2353
+ (e) => { e.preventDefault( )
2354
+ mask_add_cube( ) } )
2355
+ casalib.hotkeys( 'shift+s', { scope: source._hotkeys.id },
2356
+ (e) => { e.preventDefault( )
2357
+ mask_sub_cube( ) } )
2358
+ casalib.hotkeys( 'shift+`', { scope: source._hotkeys.id },
2359
+ (e) => { e.preventDefault( )
2360
+ ctrl.send( ids['mask-mod'],
2361
+ { scope: 'chan',
2362
+ action: 'not',
2363
+ value: { chan: source.cur_chan } },
2364
+ mask_mod_result ) } )
2365
+ casalib.hotkeys( 'shift+1', { scope: source._hotkeys.id },
2366
+ (e) => { e.preventDefault( )
2367
+ ctrl.send( ids['mask-mod'],
2368
+ { scope: 'cube',
2369
+ action: 'not',
2370
+ value: { chan: source.cur_chan } },
2371
+ mask_mod_result ) } )
2372
+ // move selection set up one pixel -- bitmask-cube mode
2373
+ //casalib.hotkeys( 'up', { scope: source._hotkeys.id },
2374
+ // (e) => { e.preventDefault( )
2375
+ // state_translate_selection( 0, 1 ) } )
2376
+ // move selection set up several pixel -- bitmask-cube mode
2377
+ //casalib.hotkeys( 'shift+up', { scope: source._hotkeys.id },
2378
+ // (e) => { e.preventDefault( )
2379
+ // const shape = source.image_source.shape
2380
+ // state_translate_selection( 0, Math.floor(shape[1]/10 ) ) } )
2381
+ // move selection set down one pixel -- bitmask-cube mode
2382
+ //casalib.hotkeys( 'down', { scope: source._hotkeys.id },
2383
+ // (e) => { e.preventDefault( )
2384
+ // state_translate_selection( 0, -1 ) } )
2385
+ // move selection set down several pixel -- bitmask-cube mode
2386
+ //casalib.hotkeys( 'shift+down', { scope: source._hotkeys.id },
2387
+ // (e) => { e.preventDefault( )
2388
+ // const shape = source.image_source.shape
2389
+ // state_translate_selection( 0, -Math.floor(shape[1]/10 ) ) } )
2390
+ // move selection set left one pixel -- bitmask-cube mode
2391
+ //casalib.hotkeys( 'left', { scope: source._hotkeys.id },
2392
+ // (e) => { e.preventDefault( )
2393
+ // state_translate_selection( -1, 0 ) } )
2394
+ // move selection set left several pixel -- bitmask-cube mode
2395
+ //casalib.hotkeys( 'shift+left', { scope: source._hotkeys.id },
2396
+ // (e) => { e.preventDefault( )
2397
+ // const shape = source.image_source.shape
2398
+ // state_translate_selection( -Math.floor(shape[0]/10 ), 0 ) } )
2399
+ // move selection set right one pixel -- bitmask-cube mode
2400
+ //casalib.hotkeys( 'right', { scope: source._hotkeys.id },
2401
+ // (e) => { e.preventDefault( )
2402
+ // state_translate_selection( 1, 0 ) } )
2403
+ // move selection set right several pixel -- bitmask-cube mode
2404
+ //casalib.hotkeys( 'shift+right', { scope: source._hotkeys.id },
2405
+ // (e) => { e.preventDefault( )
2406
+ // const shape = source.image_source.shape
2407
+ // state_translate_selection( Math.floor(shape[0]/10 ), 0 ) } )
2408
+ ''',
2409
+ 'no-bitmask-hotkey-setup': '''// next region -- no-bitmask-cube mode
2410
+ casalib.hotkeys( 'f', { scope: source._hotkeys.id },
2411
+ (e) => { source._freeze_cursor_update = true
2412
+ freeze_cb.map( (e) => e.execute( this, e ) ) } )
2413
+ //--no-mask-stale-bindings-below--------------------------------------------------
2414
+ casalib.hotkeys( 'alt+]', { scope: source._hotkeys.id },
2415
+ (e) => { e.preventDefault( )
2416
+ state_next_cursor( )} )
2417
+ // prev region -- no-bitmask-cube mode
2418
+ casalib.hotkeys( 'alt+[', { scope: source._hotkeys.id },
2419
+ (e) => { e.preventDefault( )
2420
+ state_prev_cursor( )} )
2421
+ // add region to selection -- no-bitmask-cube mode
2422
+ //casalib.hotkeys( 'alt+space, alt+/', { scope: source._hotkeys.id },
2423
+ // (e) => { e.preventDefault( )
2424
+ // state_cursor_to_selection( curmasks( ) ) } )
2425
+ // clear selection -- no-bitmask-cube mode
2426
+ //casalib.hotkeys( 'alt+escape', { scope: source._hotkeys.id },
2427
+ // (e) => { e.preventDefault( )
2428
+ // state_clear_selection( ) } )
2429
+ // delete region identified by cursor -- no-bitmask-cube mode
2430
+ //casalib.hotkeys( 'alt+del,alt+backspace', { scope: source._hotkeys.id },
2431
+ // (e) => { e.preventDefault( )
2432
+ // state_remove_mask( ) } )
2433
+ // move selection set up one pixel -- no-bitmask-cube mode
2434
+ //casalib.hotkeys( 'up', { scope: source._hotkeys.id },
2435
+ // (e) => { e.preventDefault( )
2436
+ // state_translate_selection( 0, 1 ) } )
2437
+ // move selection set up several pixel -- no-bitmask-cube mode
2438
+ //casalib.hotkeys( 'shift+up', { scope: source._hotkeys.id },
2439
+ // (e) => { e.preventDefault( )
2440
+ // const shape = source.image_source.shape
2441
+ // state_translate_selection( 0, Math.floor(shape[1]/10 ) ) } )
2442
+ // move selection set down one pixel -- no-bitmask-cube mode
2443
+ //casalib.hotkeys( 'down', { scope: source._hotkeys.id },
2444
+ // (e) => { e.preventDefault( )
2445
+ // state_translate_selection( 0, -1 ) } )
2446
+ // move selection set down several pixel -- no-bitmask-cube mode
2447
+ //casalib.hotkeys( 'shift+down', { scope: source._hotkeys.id },
2448
+ // (e) => { e.preventDefault( )
2449
+ // const shape = source.image_source.shape
2450
+ // state_translate_selection( 0, -Math.floor(shape[1]/10 ) ) } )
2451
+ // move selection set left one pixel -- no-bitmask-cube mode
2452
+ //casalib.hotkeys( 'left', { scope: source._hotkeys.id },
2453
+ // (e) => { e.preventDefault( )
2454
+ // state_translate_selection( -1, 0 ) } )
2455
+ // move selection set left several pixel -- no-bitmask-cube mode
2456
+ //casalib.hotkeys( 'shift+left', { scope: source._hotkeys.id },
2457
+ // (e) => { e.preventDefault( )
2458
+ // const shape = source.image_source.shape
2459
+ // state_translate_selection( -Math.floor(shape[0]/10 ), 0 ) } )
2460
+ // move selection set right one pixel -- no-bitmask-cube mode
2461
+ //casalib.hotkeys( 'right', { scope: source._hotkeys.id },
2462
+ // (e) => { e.preventDefault( )
2463
+ // state_translate_selection( 1, 0 ) } )
2464
+ // move selection set right several pixel -- no-bitmask-cube mode
2465
+ //casalib.hotkeys( 'shift+right', { scope: source._hotkeys.id },
2466
+ // (e) => { e.preventDefault( )
2467
+ // const shape = source.image_source.shape
2468
+ // state_translate_selection( Math.floor(shape[0]/10 ), 0 ) } )
2469
+
2470
+ // copy selection -- no-bitmask-cube mode
2471
+ casalib.hotkeys( 'alt+c', { scope: source._hotkeys.id },
2472
+ (e) => { e.preventDefault( )
2473
+ state_copy_selection( )} )
2474
+
2475
+ // paste selection current channel -- no-bitmask-cube mode
2476
+ casalib.hotkeys( 'alt+v', { scope: source._hotkeys.id },
2477
+ (e) => { e.preventDefault( )
2478
+ register_mask_change('v')
2479
+ state_paste_selection( ) } )
2480
+
2481
+ // paste selection to all channels -- no-bitmask-cube mode
2482
+ casalib.hotkeys( 'alt+shift+v', { scope: source._hotkeys.id },
2483
+ (e) => { e.preventDefault( )
2484
+ register_mask_change('V')
2485
+ for ( let stokes=0; stokes < source._chanmasks.length; ++stokes ) {
2486
+ for ( let chan=0; chan < source._chanmasks[stokes].length; ++chan ) {
2487
+ if ( stokes != source._copy_buffer[1][0] ||
2488
+ chan != source._copy_buffer[1][1] )
2489
+ state_paste_selection( curmasks( [ stokes, chan ] ) )
2490
+ }
2491
+ } } )
2492
+
2493
+ // initialize for cursor operations -- no-bitmask-cube mode
2494
+ casalib.hotkeys( '*', { keyup: true, scope: source._hotkeys.id },
2495
+ (e,handler) => { if ( e.type === 'keydown' ) {
2496
+ if ( (e.key === 'Alt' || e.key == 'Meta') && ! casalib.hotkeys.control )
2497
+ state_initialize_cursor( )
2498
+ if ( e.key === 'Control' && casalib.hotkeys.option )
2499
+ state_clear_cursor( curmasks( ) )
2500
+ }
2501
+ if ( e.type === 'keyup' ) {
2502
+ if ( e.key === 'Alt' || e.key == 'Meta' )
2503
+ state_clear_cursor( )
2504
+ if ( e.key === 'Control' && casalib.hotkeys.option )
2505
+ state_initialize_cursor( )
2506
+ } } )''',
2507
+ 'bitmask-init': '''function _create_mask_mgr( annos ) {
2508
+ const mask_anno = annos[0]
2509
+ return {
2510
+ clear: ( ) => { mask_anno.xs = [ ]
2511
+ mask_anno.ys = [ ]
2512
+ mask_region_ds.data = { xs: [ [[[]]] ], ys: [ [[[]]] ] }
2513
+ mask_region_button.icon = mask_region_icons['off'] },
2514
+ set: ( xs=[ ], ys=[ ] ) => { if ( xs.length > 0 && ys.length > 0 ) {
2515
+ mask_anno.xs = xs
2516
+ mask_anno.ys = ys
2517
+ mask_region_ds.data = { xs: [ [[[]]] ], ys: [ [[[]]] ] }
2518
+ mask_region_button.icon = mask_region_icons['off']
2519
+ } else if ( ! casalib.is_empty(contour_ds.data.xs) &&
2520
+ ! casalib.is_empty(contour_ds.data.ys) ) {
2521
+ mask_anno.xs = [ ]
2522
+ mask_anno.ys = [ ]
2523
+ mask_region_ds.data = contour_ds.data
2524
+ mask_region_ds._src_chan = source.cur_chan
2525
+ mask_region_button.icon = mask_region_icons['on']
2526
+ } else {
2527
+ if ( status ) status.text = '<p>no region found</p>'
2528
+ return
2529
+ }
2530
+ mask_anno.fill_color = 'rgba(0, 0, 0, 0)'
2531
+ mask_anno.line_width = 3
2532
+ mask_anno.line_alpha = 0.7
2533
+ mask_anno.line_dash = 'dashed'
2534
+ mask_anno.line_color = selector.color
2535
+ },
2536
+ get: ( ) => { return mask_anno }
2537
+
2538
+ }
2539
+ }
2540
+ if ( source._mask === undefined ) {
2541
+ source._mask = _create_mask_mgr( annotations )
2542
+ }''',
2543
+ 'no-bitmask-init': '''function _create_poly_mgr( annos, stokes_labels ) {
2544
+ const chan2polys = { }
2545
+
2546
+ if ( typeof document._polygon_list == 'undefined' )
2547
+ document._polygon_list = [ ]
2548
+
2549
+ const polys = document._polygon_list
2550
+
2551
+ return {
2552
+ reset_annos: ( ) => {
2553
+ source._region_annos_bimap.map(
2554
+ (anno,key) => {
2555
+ source._region_annos[anno].visible = false
2556
+ } )
2557
+ source._region_annos_bimap.clear( )
2558
+ },
2559
+ change_channel: ( ) => {
2560
+ const curstr = `${stokes_labels[source.cur_chan[0]]}${source.cur_chan[1]}`
2561
+ const curset = chan2polys.hasOwnProperty(curstr) ? chan2polys[curstr] : new EqSet( )
2562
+ const laststr = `${stokes_labels[source.last_chan[0]]}${source.last_chan[1]}`
2563
+ const lastset = chan2polys.hasOwnProperty(laststr) ? chan2polys[laststr] : new EqSet( )
2564
+ // CLEAR POLYS NO LONGER INCLUDED
2565
+ const turnoff = lastset.difference(curset)
2566
+ turnoff.forEach( (poly) => {
2567
+ const anno = source._region_annos_bimap.deleteValue(poly)
2568
+ if ( anno !== undefined ) {
2569
+ source._region_annos[anno].visible = false
2570
+ }
2571
+ } )
2572
+ // SET NEWLY VISIBLE POLYS
2573
+ const turnon = curset.difference(lastset)
2574
+ turnon.forEach( (poly) => polys[poly].display( ) )
2575
+ },
2576
+ chanset: ( chan=source.cur_chan ) => {
2577
+ const chanstr = `${stokes_labels[chan[0]]}${chan[1]}`
2578
+ if ( chan2polys.hasOwnProperty(chanstr) )
2579
+ return [...chan2polys[chanstr]].reduce( (acc,p) => {
2580
+ acc.push(polys[p])
2581
+ return acc }, [ ] )
2582
+ else return [ ]
2583
+ },
2584
+ newpoly: ( geom, chan=source.cur_chan ) => {
2585
+ const chanstr = `${stokes_labels[chan[0]]}${chan[1]}`
2586
+ if ( chan2polys.hasOwnProperty(chanstr) &&
2587
+ chan2polys[chanstr].size >= source._region_annos_bimap.bound.size ) {
2588
+ if ( status_line ) status_line.text = "<p>Sorry, max regions per channel exceeded (increase with parameter)</p>"
2589
+ return null
2590
+ }
2591
+ let id = polys.length
2592
+ let channel_set = new EqSet( [ chan ] )
2593
+ function putchans( set ) {
2594
+ if ( set instanceof Set ) {
2595
+ // (1) remove this poly from chan2polys elements for channels which are no
2596
+ // longer part of the chanset for this polygon
2597
+ // (2) remove channels from this polygon's chnanel set which are no longer part
2598
+ // of the new channel set for this polygon
2599
+ const rem = channel_set.difference( set )
2600
+ rem.forEach( (chan) => {
2601
+ const chanstr = `${stokes_labels[chan[0]]}${chan[1]}`
2602
+ if ( chan2polys.hasOwnProperty(chanstr) ) {
2603
+ chan2polys[chanstr].delete(id)
2604
+ }
2605
+ channel_set.delete(chan)
2606
+ if ( casalib.eq( chan, source.cur_chan ) ) {
2607
+ const anno = source._region_annos_bimap.deleteValue(id)
2608
+ if ( anno !== undefined ) {
2609
+ source._region_annos[anno].visible = false
2610
+ }
2611
+ }
2612
+ } )
2613
+ // (1) add this poly to new chan2polys elements for channels which are now
2614
+ // part of the chanset for this polygon
2615
+ // (2) add channels to this polygon's chnanel set which are now part
2616
+ // of the new channel set for this polygon
2617
+ const add = set.difference( channel_set )
2618
+ add.forEach( (chan) => {
2619
+ const chanstr = `${stokes_labels[chan[0]]}${chan[1]}`
2620
+ if ( chan2polys.hasOwnProperty(chanstr) ) {
2621
+ chan2polys[chanstr].add(id)
2622
+ } else {
2623
+ const set = new EqSet( [id] )
2624
+ chan2polys[chanstr] = set
2625
+ }
2626
+ channel_set.add(chan)
2627
+ if ( casalib.eq( chan, source.cur_chan ) ) {
2628
+ // if the polygon is on the current channel attach poly to
2629
+ // an anno and display it (PERHAPS the display( )
2630
+ // DEFINITION COULD BE MOVED EARLIER)
2631
+ polys[id].display( )
2632
+ }
2633
+ } )
2634
+ } else throw new Error( 'putchans requires a Set parameter' )
2635
+ return channel_set
2636
+ }
2637
+ let poly = { id, label: `rg${id}`, source,
2638
+ putchans, getchans: ( ) => { return [...channel_set] },
2639
+ styling: { line: { color: styling.line.color.color,
2640
+ width: styling.line.width.value,
2641
+ alpha: styling.line.alpha.value,
2642
+ dash: styling.line.dash.label },
2643
+ fill: { color: styling.fill.color.color,
2644
+ alpha: styling.fill.alpha.value,
2645
+ hatch: styling.fill.hatch.label } } }
2646
+ if ( typeof(geom) == 'object' ) {
2647
+ if ( 'xs' in geom && 'ys' in geom ) {
2648
+ poly.type = 'poly'
2649
+ poly.geometry = { xs: [ ].slice.call(geom.xs), ys: [ ].slice.call(geom.ys) }
2650
+ } else if ( 'x0' in geom && 'x1' in geom &&
2651
+ 'y0' in geom && 'y1' in geom ) {
2652
+ poly.type = 'rect'
2653
+ poly.gometry = { xs: [ geom.x0, geom.x0, geom.x1, geom.x1 ],
2654
+ ys: [ geom.y0, geom.y1, geom.y1, geom.y0 ] }
2655
+ } else {
2656
+ throw new Error(`newpoly: unrecognized "geom" format ${geom}`)
2657
+ }
2658
+ } else {
2659
+ throw new Error(`newpoly: geom paramter should be an object instead of ${typeof(geom)}`)
2660
+ }
2661
+ poly['display'] = ( anno_id=-1 ) => {
2662
+ function update_anno( a_id ) {
2663
+ const anno = source._region_annos[a_id]
2664
+ anno._poly = poly
2665
+ anno.xs = poly.geometry.xs.slice( )
2666
+ anno.ys = poly.geometry.ys.slice( )
2667
+ anno.visible = true
2668
+
2669
+ const fill = poly.styling.fill
2670
+ const line = poly.styling.line
2671
+ anno.fill_color = fill.color
2672
+ anno.fill_alpha = fill.alpha
2673
+ anno.hatch_pattern = fill.hatch
2674
+ anno.line_color = line.color
2675
+ anno.line_width = line.width
2676
+ anno.line_alpha = line.alpha
2677
+ anno.line_dash = line.dash
2678
+ }
2679
+
2680
+ if ( anno_id >= 0 ) {
2681
+ update_anno(anno)
2682
+ return true
2683
+ } else {
2684
+ const anno = source._region_annos_bimap.add(id)
2685
+ if ( anno !== undefined ) {
2686
+ update_anno(anno)
2687
+ return true
2688
+ }
2689
+ return false
2690
+ }
2691
+ }
2692
+ if ( ! chan2polys.hasOwnProperty(chanstr) ) chan2polys[chanstr] = new EqSet( )
2693
+ chan2polys[chanstr].add(id)
2694
+ polys.push(poly)
2695
+ return poly
2696
+ },
2697
+ list: ( ) => { return polys }
2698
+ }
2699
+ }
2700
+ if ( source._polys === undefined ) {
2701
+ source._polys = _create_poly_mgr(annotations,stokes_labels)
2702
+ }
2703
+ if ( source._stokes === undefined ) {
2704
+ source._stokes = { }
2705
+ }
2706
+ if ( source._mask_breadcrumbs === undefined ) {
2707
+ source._mask_breadcrumbs = ''
2708
+ }
2709
+ if ( source._cur_chan_prev === undefined ) {
2710
+ source._cur_chan_prev = source.cur_chan
2711
+ }
2712
+ if ( source._oldpolys === undefined ) {
2713
+ source._oldpolys = [ ] // [ { id: int,
2714
+ // type: string,
2715
+ // geometry: { xs: [ float, ... ], ys: [ float, ... ] } } ]
2716
+
2717
+ source._region_annos = annotations
2718
+ source._region_annos_bimap = new BoundedBiMap( new Set(Array.from(annotations.keys())) )
2719
+
2720
+ source._cursor_color = 'white'
2721
+ source._default_color = 'black'
2722
+ source._annotation_ids = annotations.reduce( (acc,c) => { acc.push(c.id); return acc }, [ ] )
2723
+ source._annotations = annotations.reduce( (acc,c) => { acc[c.id] = c; return acc }, { } )
2724
+ source._annos = annotations
2725
+ source._annos.forEach( (c) => { c.visible = false; c._poly = null } )
2726
+ // OPT sets the cursor on the first poly
2727
+ source._cursor = -1 // OPT-n or OPT-p move the _cursor through polys for current channel
2728
+ // OPT-SPACE adds cursor to _selections
2729
+ // OPT-c copies _selections to the _copy_buffer and clears _selections
2730
+ // OPT-v pastes _copy_buffer to a new channel
2731
+ // OPT-SHIFT-v pastes _copy_buffer to ALL channels
2732
+ // OPT-up moves _selections up
2733
+ // OPT-down moves _selections down
2734
+ // OPT-left moves _selections left
2735
+ // OPT-right moves _selections right
2736
+ // OPT-delete removes mask highlighted by _cursor
2737
+ // OPT-CTRL-up moves to next channel
2738
+ // OPT-CTRL-down moves to previous channel
2739
+ // OPT-CTRL-left moves to previous stokes channel
2740
+ // OPT-CTRL-right moves to next stokes channel
2741
+ }
2742
+ if ( source._copy_buffer === undefined ) {
2743
+ // Buffer that will contain copied selection so that the selection from one channel can be pasted
2744
+ // into this channel, another channel, or multiple channels...
2745
+ // vvvvvvvvvv------------------------------------------- polygon index to be pasted
2746
+ // [ [ [ POLY-INDEX, [ dx, dy ] ], ... ], [ stokes, chan ] ]
2747
+ // ^^^^^^^^^^ ^^^^^^^^^^^^^^^^---- image plane origin of copy
2748
+ // |
2749
+ // +------------------------------- x,y delta translation for the copied poly
2750
+ // The poly index _copy_buffer[0][X] is the polygon that should be pasted and
2751
+ // the translation _copy_buffer[1][X] is the translation that should be applied
2752
+ // to the pasted polygon (using a newly aquired annotation from the cache)
2753
+ source._copy_buffer = [ [ ], [ ] ]
2754
+ }
2755
+ if ( source._chanmasks === undefined ) {
2756
+ // Primary axis: stokes
2757
+ // Secondary axis: frequency vvvvvvvvvvvvvvvvvvv------ polygons drawn on this channel
2758
+ // [ [ [ANNOTATION-ID, ... ], [ POLY-INDEX, ... ], [ [ dx, dy ], ... ], [ INDEX, ... ], CHAN ], ... ]
2759
+ // ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^---- selection indexes (SELECTION SET)
2760
+ // | | (INDEX into previous 3 lists)
2761
+ // | | (KEEP OR REMOVE???)
2762
+ // | +------ x,y delta translation (**TO*BE*REMOVED**)
2763
+ // |
2764
+ // +--------------------------- annotations in use for this channel
2765
+ // one for each polygon
2766
+ //
2767
+ // one-to-one correspondence between annotation and poly indexes
2768
+ source._chanmasks = [ ]
2769
+ let stokes = source.num_chans[0]
2770
+ while(stokes-- > 0) {
2771
+ source._chanmasks[stokes] = [ ]
2772
+ let chan = source.num_chans[1]
2773
+ while(chan-- > 0) { source._chanmasks[stokes][chan] = [ [ ], [ ], null, [ ], [ stokes, chan ] ] }
2774
+ } // **REMOVE*WHEN*POSSIBLE**-------------------------^^^^--^^^^
2775
+ }''',
2776
+ 'no-bitmask-tool-selection':'''function degenerate( geom ) {
2777
+ const area = geom.type == 'poly' ? casalib.polyArea( geom.sx, geom.sy ) :
2778
+ geom.type == 'rect' ? casalib.polyArea( [ [geom.x0, geom.y0],
2779
+ [geom.x0, geom.y1],
2780
+ [geom.x1, geom.y1],
2781
+ [geom.x1, geom.y0] ] ) : 0
2782
+ return area == 0
2783
+ }
2784
+ if ( source.masking_on( ) ) {
2785
+ let poly
2786
+ const geometry = cb_obj['geometry']
2787
+ /*** filter degenerate polygons ***/
2788
+ if ( ! degenerate(geometry) ) {
2789
+ if ( geometry.type === 'rect' ) {
2790
+ poly = source._polys.newpoly( geometry )
2791
+ if ( poly && poly.display( ) ) {
2792
+ register_mask_change('r')
2793
+ const chan = source.cur_chan
2794
+ chan_select[chan[0]].value = chan[1].toString( )
2795
+ } else if ( status_line ) status_line.text = "<p>Sorry, region display failed</p>"
2796
+ } else if ( geometry.type === 'poly' && cb_obj.final ) {
2797
+ poly = source._polys.newpoly( { xs: geometry.x, ys: geometry.y } )
2798
+ if ( poly && poly.display( ) ) {
2799
+ register_mask_change('p')
2800
+ const chan = source.cur_chan
2801
+ chan_select[chan[0]].value = chan[1].toString( )
2802
+ } else if ( status_line ) status_line.text = "<p>Sorry, region display failed</p>"
2803
+ }
2804
+ }
2805
+ }'''
2806
+ }
2807
+
2808
+ def span_update( span1, span2 ):
2809
+ return f'''if ( ! {span1}._edited ) {{
2810
+ {span1}._editing = true
2811
+ if ( {span1}.location <= {span2}.location ) {{
2812
+ {span1}.location = histogram.data_source.data.left[0]
2813
+ min.value = {span1}.location.toString( )
2814
+ }} else {{
2815
+ {span1}.location = histogram.data_source.data.right[histogram.data_source.data.right.length-1]
2816
+ max.value = {span1}.location.toString( )
2817
+ }}
2818
+ {span1}._editing = false
2819
+ }}
2820
+ '''
2821
+
2822
+ self._js = { ### ImagePipe initialization code which manages the shift-key behavior which swiches between
2823
+ ### addition/subtraction to a single channel VS add/sub from all channels of the cube...
2824
+ 'cube-init': '''add._mode = 'chan'
2825
+ sub._mode = 'chan'
2826
+ let foo = casalib.is_empty
2827
+ function cube_on( ) {
2828
+ add._mode = 'cube'
2829
+ add.icon = img['add']['cube']
2830
+ sub._mode = 'cube'
2831
+ sub.icon = img['sub']['cube']
2832
+ }
2833
+ function cube_off( ) {
2834
+ add._mode = 'chan'
2835
+ add.icon = img['add']['chan']
2836
+ sub._mode = 'chan'
2837
+ sub.icon = img['sub']['chan']
2838
+ }
2839
+ casalib.hotkeys( '*',
2840
+ { keyup: true, scope: 'all' },
2841
+ (e) => {
2842
+ if ( e.key == "Shift" ) {
2843
+ if ( e.type == 'keyup' )
2844
+ cube_off( )
2845
+ else
2846
+ cube_on( )
2847
+ } } )
2848
+ casalib.hotkeys( '*',
2849
+ { keyup: true, scope: '%s' },
2850
+ (e) => {
2851
+ if ( e.key == "Shift" ) {
2852
+ if ( e.type == 'keyup' )
2853
+ cube_off( )
2854
+ else
2855
+ cube_on( )
2856
+ } } )''' % self._hotkey_state['id'],
2857
+ ### update stats in response to channel changes
2858
+ ### -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
2859
+ ### The slider and the events from the update of the image source are
2860
+ ### coupled because the image cube channel can be changed outside of the
2861
+ ### slider context.
2862
+ ###
2863
+ ### (1) moving the slider updates the channel
2864
+ ### (2) when the channel is changed using a hotkey the slider must be updated
2865
+ ###
2866
+ ### to fix a problem where moving the slider very quickly caused oscillation between
2867
+ ### two slider values (slider would just cycle back and forth) these two related
2868
+ ### updates are separated by the hotkeys scope. When the scope is source._hotkeys.id then
2869
+ ### the cursor is inside, hotkeys are active (and slider is updated). When outside
2870
+ ### and the scope is not equal to source._hotkeys.id, the slider updates the channel.
2871
+ ###
2872
+ 'pixel-update-func': ''' function refresh_pixel_display( index, intensity, masked, world_coord=true ) {
2873
+ const digits = 5
2874
+ function exponential( x ) {
2875
+ try { return x.toExponential(digits) }
2876
+ catch(e) {
2877
+ console.group( 'EXPONENTIAL' )
2878
+ console.log( `oops, toExponential error for "${x}": ${e}` )
2879
+ console.log((new Error()).stack)
2880
+ console.groupEnd( )
2881
+ }
2882
+ return String(x)
2883
+ }
2884
+ if ( world_coord ) {
2885
+ const pt = new casalib.coordtxl.Point2D( Number(index[0]), Number(index[1]) )
2886
+ isource.wcs( ).imageToWorldCoords(pt,false)
2887
+ let wcstr = new casalib.coordtxl.WorldCoords(pt.getX(),pt.getY()).toString( )
2888
+ pixlabel.text = '<p ALIGN=RIGHT>' + wcstr + "</p><p ALIGN=RIGHT>" + exponential(intensity) +
2889
+ (masked === undefined ? '' : masked ? " <b>masked</b>" : " <b>unmasked</b>") + '</p>'
2890
+ } else {
2891
+ pixlabel.text = '<p ALIGN=RIGHT>' + index[0] + ', ' + Number(index[1]) +
2892
+ "</p><p ALIGN=RIGHT>" + exponential(intensity) +
2893
+ (masked ? " <b>masked</b>" : " <b>unmasked</b>") + '</p>'
2894
+ }
2895
+ }
2896
+ function update_spectrum( _chan, _index, update_func ) {
2897
+ function array_equal( a1, a2 ) {
2898
+ return (a1.length == a2.length) && a1.every((element, index) => element === a2[index])
2899
+ }
2900
+ if ( isource._update_spectrum &&
2901
+ _chan[0] == isource._update_spectrum.chan[0] &&
2902
+ array_equal( _index, isource._update_spectrum.index ) ) {
2903
+ update_func( { ...isource._update_spectrum, chan: _chan } )
2904
+ } else {
2905
+ function _update_spectrum ( msg ) {
2906
+ if ( msg.update &&
2907
+ 'spectrum' in msg.update &&
2908
+ 'index' in msg.update &&
2909
+ 'chan' in msg.update &&
2910
+ msg.update.index.length == 2 &&
2911
+ msg.update.index.length == 2 ) {
2912
+ isource._update_spectrum = msg.update
2913
+ update_func( isource._update_spectrum )
2914
+ } else console.log( 'Error: update of spectrum', msg )
2915
+ }
2916
+ if ( isource._current_pos )
2917
+ ctrl.send( ids['fetch-spectrum'],
2918
+ { action: 'spectrum',
2919
+ value: { chan: _chan, index: isource._current_pos } },
2920
+ _update_spectrum, true )
2921
+ }
2922
+ }''',
2923
+ 'contour-maskmod': ''' function maskmod_region_clear( ) {
2924
+ annotations[0].xs = [ ]
2925
+ annotations[0].ys = [ ]
2926
+ mask_region_ds.data = { xs: [ [[[]]] ], ys: [ [[[]]] ] }
2927
+ mask_region_button.icon = mask_region_icons['off']
2928
+ }
2929
+ function maskmod_region_set( region, xs=[ ], ys=[ ] ) {
2930
+ if ( xs.length > 0 && ys.length > 0 ) {
2931
+ annotations[0].xs = xs
2932
+ annotations[0].ys = ys
2933
+ mask_region_ds.data = { xs: [ [[[]]] ], ys: [ [[[]]] ] }
2934
+ mask_region_button.icon = mask_region_icons['off']
2935
+ } else if ( ! casalib.is_empty(contour_ds.data.xs) &&
2936
+ ! casalib.is_empty(contour_ds.data.ys) ) {
2937
+ annotations[0].xs = [ ]
2938
+ annotations[0].ys = [ ]
2939
+ mask_region_ds.data = contour_ds.data
2940
+ mask_region_ds._src_chan = source.cur_chan
2941
+ mask_region_button.icon = mask_region_icons['on']
2942
+ } else {
2943
+ if ( status ) status.text = '<p>no region found</p>'
2944
+ return
2945
+ }
2946
+ region.fill_color = 'rgba(0, 0, 0, 0)'
2947
+ region.line_width = 3
2948
+ region.line_alpha = 0.7
2949
+ region.line_dash = 'dashed'
2950
+ region.line_color = selector.color
2951
+ }''',
2952
+ 'slider_w_stats': '''if ( casalib.hotkeys.getScope( ) !== isource._hotkeys.id ) {
2953
+ isource.channel( slider.value, isource.cur_chan[0],
2954
+ msg => { if ( 'stats' in msg ) { isource.update_statistics( msg.stats ) }
2955
+ if ( 'hist' in msg ) {
2956
+ %s
2957
+ %s
2958
+ } } )
2959
+ if ( isource._current_pos )
2960
+ update_spectrum( [isource.cur_chan[0], slider.value], isource._current_pos,
2961
+ ( spec ) => {
2962
+ if ( isource.masking_on( ) )
2963
+ refresh_pixel_display( spec.index,
2964
+ 'mask' in spec ? spec.mask[spec.chan[1]] : undefined,
2965
+ 'mask' in spec && spec.mask[spec.chan[1]],
2966
+ pix_wrld && pix_wrld.label == 'pixel' ? false : true )
2967
+ } )
2968
+ if ( go_to && ! go_to._has_focus ) {
2969
+ go_to.value = String( slider.value )
2970
+ }
2971
+ }''' % ( span_update( 'span1', 'span2' ), span_update( 'span2', 'span1' ) ),
2972
+ ###
2973
+ ### >>HERE>> This code has drifted out of date... so creating a display WITHOUT statistics
2974
+ ### will result in errors without JavaScript
2975
+ ###
2976
+ 'slider_wo_stats': '''if ( casalib.hotkeys.getScope( ) !== isource._hotkeys.id ) {
2977
+ isource.channel( slider.value, isource.cur_chan[0],
2978
+ msg => { if ( 'hist' in msg ) {
2979
+ %s
2980
+ %s
2981
+ } } )
2982
+ }''' % ( span_update( 'span1', 'span2' ), span_update( 'span2', 'span1' ) ),
2983
+ ### initialize mask state
2984
+ ###
2985
+ ### mask breadcrumbs
2986
+ ###
2987
+ 'mask-state-init': self._js_mode_code['no-bitmask-init'] if self._mask_path is None else self._js_mode_code['bitmask-init'],
2988
+ ###
2989
+ ### code to update stokes after stokes selection from dropdown
2990
+ ###
2991
+ 'stokes-change': '''if ( cb_obj.item != stokes.label ) {
2992
+ source.channel( source.cur_chan[1], %s,
2993
+ msg => { stokes.label = cb_obj.item
2994
+ if ( goto_stokes ) { goto_stokes.label = `${cb_obj.item} Channel` }
2995
+ if ( 'stats' in msg ) { source.update_statistics( msg.stats ) }
2996
+ } )
2997
+ }''',
2998
+ ### function to return mask state for current channel, the 'source' (image_data_source) object
2999
+ ### is parameterized so that this can be used in callbacks where 'cb_obj' is set by Bokeh to
3000
+ ### point to our 'source' object
3001
+ 'func-curmasks': lambda source="source": '''
3002
+ function register_mask_change( code ) {
3003
+ source._mask_breadcrumbs += code
3004
+ }
3005
+ function curmasks( cur=source.cur_chan ) { return source._chanmasks[cur[0]][cur[1]] }
3006
+ function chanfold( stokes, func, init ) {
3007
+ let result = init
3008
+ for ( let chan=0; chan < source._chanmasks[stokes].length; ++chan ) {
3009
+ result = func( result, curmasks( [stokes, chan] ), [ stokes, chan ] )
3010
+ }
3011
+ return result
3012
+ }
3013
+ function collect_channel_selection( cur=source.cur_chan ) {
3014
+ if ( typeof(cur) == 'number' ) {
3015
+ cur = [ source.cur_chan[0], cur ] // accept just channel (with stokes implied)
3016
+ }
3017
+ const cm = curmasks( cur )
3018
+ var details = [ ]
3019
+ var polys = new EqSet( )
3020
+ cm[3].forEach( s => {
3021
+ details.push( { p: cm[1][s], d: [ ].slice.call(cm[2][s]) } )
3022
+ polys.add( cm[1][s] )
3023
+ } )
3024
+ return polys.size == 0 ? { masks: [ ], polys: [ ] } :
3025
+ { masks: [ [ [ ].slice.call(cur), details ] ],
3026
+ polys: Array.from(polys).reduce( (acc,p) => {
3027
+ acc.push([ p, (({ type, geometry }) => ({ type, geometry }))(source._oldpolys[p]) ])
3028
+ // ^^^^^^^^^^^filter^type^and^geometry^^^^^^^^^^^
3029
+ return acc
3030
+ }, [ ] ) }
3031
+ }
3032
+ function collect_masks( ) {
3033
+ return casalib.reduce( (acc, poly) => {
3034
+ acc[poly.label] = {
3035
+ //channels: poly.getchans( ),
3036
+ //geometry: poly.geometry,
3037
+ styling: poly.styling }
3038
+ return acc }, source._polys.list( ), { } )
3039
+ }'''.replace('source',source),
3040
+ ### create a new polygon -- create state and establish a correspondence between the polygon and the
3041
+ ### annotation that will be used to represent it for this particular channel
3042
+ ###
3043
+ ### NEWPOLY should not be used for EXISTING polygons that are placed on different channels because it
3044
+ ### creates a new poly_id but the polygon isn't being changed... if an existing polygon is changed
3045
+ ### it changes for all channels that have it
3046
+ ###
3047
+ 'func-newpoly': '''function newpoly( cm ) {
3048
+ let anno_id = source._annotation_ids.find( v => ! cm[0].includes(v) ) // find id not in use
3049
+ if ( typeof(anno_id) != 'string') {
3050
+ return null // is id reasonable
3051
+ }
3052
+ let poly_id = source._oldpolys.length // all polygons created
3053
+ let poly = { id: poly_id,
3054
+ type: null,
3055
+ geometry: { } }
3056
+ let result = [ source._annotations[anno_id], poly ]
3057
+ source._oldpolys.push(poly) // all polygons created
3058
+ cm[0].push(anno_id) // annotations in use by this channel
3059
+ cm[1].push(poly_id) // polygons in use by this channel
3060
+ if ( region_newpoly ) region_newpoly.execute( cb_obj, result )
3061
+ return result
3062
+ }''',
3063
+ ### functions for updating the polygon/annotation state in response to key presses
3064
+ ### ------------------------------------------------------------------------------------------
3065
+ ### state_update_cursor -- move cursor to specified polygon index
3066
+ ### state_clear_cursor -- unset cursor state (e.g. option release or control pressed)
3067
+ ### state_next_cursor -- move the cursor to the next (with wrapping) polygon index
3068
+ ### state_prev_cursor -- move the cursor to the previous (with wrapping) polygon index
3069
+ ### state_nonselection_cursor -- move the cursor to another polygon index which is not in the
3070
+ ### selection set (avoid always picking the next index with the
3071
+ ### expectation that the user will be adding more polygons to the
3072
+ ### selection set (and avoiding always going to the one not desired)
3073
+ ### state_cursor_to_selection -- add the current cursor polygon to the selection set
3074
+ ### state_clear_selection -- clear selection set
3075
+ ### state_initialize_cursor -- set cursor to its initial value
3076
+ ### state_translate_selection -- move all polygons in the selection set by an x/y translation
3077
+ ### state_copy_selection -- replace copy buffer contents with the contents of the selection set
3078
+ ### state_paste_selection -- past the contents of the copy buffer into the current channel
3079
+ ### if the polygons in the copy buffer already exist in the current
3080
+ ### paste polygons with an x/y translation (to avoid pasting on top)
3081
+ 'key-state-funcs': '''function state_update_cursor( index, cm = curmasks( ) ) {
3082
+ if ( index >= 0 && index < cm[0].length ) {
3083
+ source._annotations[cm[0][index]].line_color = source._cursor_color
3084
+ source._cursor = index
3085
+ } else source._cursor = -1
3086
+ }
3087
+ function state_clear_cursor( cm = curmasks( ) ) {
3088
+ if ( source._cursor >= 0 && source._cursor < cm[0].length ) {
3089
+ const result = source._cursor
3090
+ source._annotations[cm[0][result]].line_color = null
3091
+ source._cursor = -1
3092
+ return result
3093
+ } else {
3094
+ source._cursor = -1
3095
+ return cm[0].length >= 0 ? 0 : -1
3096
+ }
3097
+ }
3098
+ function state_next_cursor( cm = curmasks( ) ) {
3099
+ const cursor = state_clear_cursor( cm )
3100
+ if ( cursor >= 0 && cursor + 1 < cm[0].length )
3101
+ state_update_cursor( cursor + 1, cm )
3102
+ else
3103
+ state_update_cursor( 0, cm )
3104
+ }
3105
+ function state_prev_cursor( cm = curmasks( ) ) {
3106
+ const cursor = state_clear_cursor( cm )
3107
+ if ( cursor >= 0 && cursor - 1 >= 0 )
3108
+ state_update_cursor( cursor - 1, cm )
3109
+ else
3110
+ state_update_cursor( cm[0].length - 1, cm )
3111
+ }
3112
+ function state_nonselection_cursor( cm = curmasks( ) ) {
3113
+ let tried_indexes = [ ]
3114
+ while ( tried_indexes.length < cm[0].length ) {
3115
+ // just looping through possible indexes results in always going to an
3116
+ // early index which is annoying if it is not one of the ones the user
3117
+ // is interested in changing...
3118
+ const index = Math.floor(Math.random()*cm[0].length)
3119
+ if ( ! tried_indexes.includes(index) ) {
3120
+ if ( ! cm[3].includes(index) ) {
3121
+ return index
3122
+ } else {
3123
+ tried_indexes.push(index)
3124
+ }
3125
+ }
3126
+ }
3127
+ return source._cursor
3128
+ }
3129
+ function state_cursor_to_selection( cm = curmasks( ) ) {
3130
+ const cursor = state_clear_cursor( cm )
3131
+ if ( cursor >= 0 && cursor < cm[0].length ) {
3132
+ const index = cm[3].indexOf(cursor);
3133
+ if ( index > -1 ) {
3134
+ const new_cursor = state_nonselection_cursor( cm ) // find next cursor before removing from selection (to avoid current cursor)
3135
+ source._annotations[cm[0][cursor]].fill_color = source._default_color // remove selection background color
3136
+ cm[3].splice( index, 1 ) // remove cursor that is already in selection buffer
3137
+ if ( new_cursor >= 0 )
3138
+ state_update_cursor( new_cursor, cm ) // advance cursor (for adding multiple mask regions)
3139
+ else
3140
+ state_update_cursor( cursor, cm ) // restore the old cursor (because no non-selected cursor is available)
3141
+ } else {
3142
+ source._annotations[cm[0][cursor]].fill_color = source._cursor_color // background changes for masks in selection buffer
3143
+ cm[3].push(cursor) // add cursor to selection buffer
3144
+ const new_cursor = state_nonselection_cursor( cm ) // find next cursor after adding to selection (to avoid current cursor)
3145
+ if ( new_cursor >= 0 )
3146
+ state_update_cursor( new_cursor, cm ) // advance cursor (for adding multiple mask regions)
3147
+ else
3148
+ state_update_cursor( cursor, cm ) // restore the old cursor (because no non-selected cursor is available)
3149
+ }
3150
+ }
3151
+ }
3152
+ function state_clear_selection( cm = curmasks( ) ) {
3153
+ // reset selection backgrounds
3154
+ cm[3].forEach( s => source._annotations[cm[0][s]].fill_color = source._default_color )
3155
+ // clear selection buffer
3156
+ cm[3].length = 0
3157
+ }
3158
+ function state_initialize_cursor( ) {
3159
+ state_update_cursor( 0, curmasks( ) )
3160
+ }
3161
+ function state_remove_mask( cm = curmasks( ) ) {
3162
+ register_mask_change('D')
3163
+ const cursor = source._cursor
3164
+ const index = cm[3].indexOf(cursor);
3165
+ if ( index > -1 ) {
3166
+ cm[3].splice( index, 1 ) // remove cursor that is already in selection buffer
3167
+ source._annotations[cm[0][cursor]].fill_color = source._default_color // background changes for masks in selection buffer
3168
+ }
3169
+ source._annotations[cm[0][cursor]].xs = [ ] // reset x coordinates
3170
+ source._annotations[cm[0][cursor]].ys = [ ] // reset y coordinates
3171
+ source._annotations[cm[0][cursor]].fill_color = source._default_color // reset background
3172
+ source._annotations[cm[0][cursor]].line_color = null // remove line
3173
+ cm[0].splice( cursor, 1 )
3174
+ cm[1].splice( cursor, 1 )
3175
+ state_initialize_cursor( )
3176
+ }
3177
+ function state_translate_selection( dx, dy, cm = curmasks( ) ) {
3178
+ register_mask_change('T')
3179
+ const shape = source.image_source.shape
3180
+ for ( const s of cm[3] ) {
3181
+ if ( dx > 0 && (Math.ceil(Math.max( ...source._annotations[cm[0][s]].xs )) + dx) >= shape[0] ) return;
3182
+ if ( dy > 0 && (Math.ceil(Math.max( ...source._annotations[cm[0][s]].ys )) + dy) >= shape[1] ) return;
3183
+ if ( dx < 0 && (Math.floor(Math.min( ...source._annotations[cm[0][s]].xs )) + dx) <= 0 ) return;
3184
+ if ( dy < 0 && (Math.floor(Math.min( ...source._annotations[cm[0][s]].ys )) + dy) <= 0 ) return;
3185
+ }
3186
+ cm[3].forEach( s => {
3187
+ if ( dx !== 0 ) source._annotations[cm[0][s]].xs = source._annotations[cm[0][s]].xs.map( x => x + dx )
3188
+ if ( dy !== 0 ) source._annotations[cm[0][s]].ys = source._annotations[cm[0][s]].ys.map( y => y + dy )
3189
+ cm[2][s][0] += dx
3190
+ cm[2][s][1] += dy
3191
+ } )
3192
+ }
3193
+ function state_copy_selection( cm = curmasks( ) ) {
3194
+ source._copy_buffer = [ [ ], [ ].slice.call(source.cur_chan) ]
3195
+ // polygons----------^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^----the channel polys were copied from
3196
+ // loop over selection indexes and add polygon index + translation
3197
+ cm[3].forEach( idx => { source._copy_buffer[0].push( [ cm[1][idx], [ ].slice.call(cm[2][idx]) ] ) } )
3198
+ }
3199
+ function state_paste_selection( cm = curmasks( ) ) {
3200
+ function paste( c_indexes ) {
3201
+ c_indexes.forEach( idx => {
3202
+ // idx[0] -- index into the master polygon list
3203
+ // idx[1][0] -- x offset for this polygon
3204
+ // idx[1][1] -- y offset for this polygon
3205
+ let anno_id = source._annotation_ids.find( v => ! cm[0].includes(v) )
3206
+ if ( typeof(anno_id) == 'string') { // is id reasonable
3207
+ cm[0].push(anno_id) // claim this annotation
3208
+ cm[3].push(cm[1].length) // add pasted poly to selected set
3209
+ // (position in channel poly list)
3210
+ cm[1].push(idx[0]) // add poly index (index may occur
3211
+ // more than once)
3212
+ const poly = source._oldpolys[idx[0]]
3213
+ if ( cm[4][0] == source.cur_chan[0] && cm[4][1] == source.cur_chan[1] ) {
3214
+ const anno = source._annotations[anno_id]
3215
+ anno.xs = poly.geometry.xs.map( x => x + idx[1][0] )
3216
+ anno.ys = poly.geometry.ys.map( y => y + idx[1][1] )
3217
+ anno.fill_color = source._cursor_color // change fill color for addition
3218
+ // to selected set (above)
3219
+ }
3220
+ }
3221
+ } )
3222
+ }
3223
+ function paste_with_offset( c_indexes ) {
3224
+ // find extent of the polygons to be pasted that already exist in this channel
3225
+ function calculate_offset( ) {
3226
+ function minmax( ) {
3227
+ return c_indexes.reduce( (acc,i) => {
3228
+ // i[0] -- index into the master polygon list
3229
+ // i[1][0] -- x offset for this polygon
3230
+ // i[1][1] -- y offset for this polygon
3231
+ const min = [ Math.min(...source._oldpolys[i[0]].geometry.xs) + i[1][0],
3232
+ Math.min(...source._oldpolys[i[0]].geometry.ys) + i[1][1] ]
3233
+ const max = [ Math.max(...source._oldpolys[i[0]].geometry.xs) + i[1][0],
3234
+ Math.max(...source._oldpolys[i[0]].geometry.ys) + i[1][1] ]
3235
+ return acc[0] == null ? [ min, max ] :
3236
+ [ [ Math.min( min[0], acc[0][0] ), Math.min( min[1], acc[1][1] ) ],
3237
+ [ Math.max( max[0], acc[1][0] ), Math.max( max[1], acc[1][1] ) ] ]
3238
+ }, [ null, null ] )
3239
+ }
3240
+ const offset = 35
3241
+ const shape = source.image_source.shape
3242
+ const [[xmin,ymin],[xmax,ymax]] = minmax( )
3243
+ if ( xmax + offset < shape[0] &&
3244
+ ymax + offset < shape[1] ) return [ offset, offset ]
3245
+ else if ( xmin - offset >= 0 &&
3246
+ ymin - offset >= 0 ) return [ -offset, -offset ]
3247
+ else if ( xmin - offset >= 0 &&
3248
+ ymax + offset < shape[1] ) return [ -offset, offset ]
3249
+ else if ( xmax + offset < shape[0] &&
3250
+ ymin - offset >= 0 ) return [ offset, -offset ]
3251
+ else if ( xmax + offset < shape[0] ) return [ offset, 0 ]
3252
+ else if ( xmin - offset >= 0 ) return [ -offset, 0 ]
3253
+ else if ( ymax + offset < shape[1] ) return [ 0, offset ]
3254
+ else if ( ymin - offset >= 0 ) return [ 0, -offset ]
3255
+ else return [ 0, 0 ]
3256
+ }
3257
+ if ( c_indexes.length > 0 ) {
3258
+ const off = calculate_offset( )
3259
+ paste( c_indexes.map( e => [ e[0], [e[1][0]+off[0],e[1][1]+off[1]] ] ) )
3260
+ }
3261
+ }
3262
+ function groupby( source, predicate ) {
3263
+ return source.reduce( (acc,elem) => {
3264
+ if ( predicate(elem) ) acc[0].push(elem)
3265
+ else acc[1].push(elem)
3266
+ return acc }, [ [], [] ] )
3267
+ }
3268
+ function poly_already_exists( c_index ) {
3269
+ const c_geom = source._oldpolys[c_index[0]].geometry
3270
+ const c_delta = c_index[1]
3271
+ const potential_matching_chan_polys = cm[1].reduce( (acc,i,cm_index) => {
3272
+ // first element of return is the index into the master list of polygons
3273
+ // the second is the index into the curmask masks
3274
+ if ( source._oldpolys[i].geometry.xs.length == c_geom.xs.length ) acc.push([i,cm_index])
3275
+ return acc
3276
+ }, [ ] )
3277
+ return potential_matching_chan_polys.reduce( (acc,p) => {
3278
+ const match_delta = cm[2][p[1]]
3279
+ if ( c_geom.xs.every( (element, i) => element === source._oldpolys[p[0]].geometry.xs[i] ) &&
3280
+ c_geom.ys.every( (element, i) => element === source._oldpolys[p[0]].geometry.ys[i] ) &&
3281
+ c_delta[0] == match_delta[0] && c_delta[1] == match_delta[1] )
3282
+ acc.push(p)
3283
+ return acc}, [ ] ).length > 0
3284
+ }
3285
+ const addition = groupby( source._copy_buffer[0], poly_already_exists )
3286
+ paste_with_offset( addition[0] )
3287
+ paste( addition[1] )
3288
+ }''',
3289
+ ### add a polygon after one is drawn by the user via the Bokeh annotation tools
3290
+ 'refresh-regions': '''//cb_obj._polys.reset_annos( )
3291
+ //cb_obj._polys.chanset( ).map( p => p.display( ) )
3292
+ cb_obj._polys.change_channel( )
3293
+ ''',
3294
+ 'add-polygon': '''const cur_masks = curmasks( )
3295
+ const prev_masks = curmasks(cb_obj._cur_chan_prev)
3296
+ prev_masks[0].forEach( (i) => { // RESET ANNOTATIONS FROM OLD CHANNEL
3297
+ cb_obj._annotations[i].xs = [ ]; // clear Xs for annotations used by prev
3298
+ cb_obj._annotations[i].ys = [ ] // clear Ys for annotations used by prev
3299
+ } )
3300
+ cur_masks[1].forEach( ( poly_id, i ) => {
3301
+ const mask_annotation = cb_obj._annotations[cur_masks[0][i]] // current annotation
3302
+ const mask_poly = cb_obj._oldpolys[poly_id] // current poly
3303
+ if ( mask_poly ) { // sometimes this is undefined when
3304
+ mask_annotation.xs = mask_poly.geometry.xs.slice( ) // set Xs for the current annotation
3305
+ mask_annotation.ys = mask_poly.geometry.ys.slice( ) // set Ys for the current annotation
3306
+ } } )
3307
+ cb_obj._cur_chan_prev = cb_obj.cur_chan;''',
3308
+ ### invoke key state management functions in response to keyboard events in the document. this
3309
+ ### code manages the permitted key combinations while most of the state management is handled
3310
+ ### by the state management functions.
3311
+ ###
3312
+ ### //****************************************************
3313
+ ### //*** NEED TO IMPLEMENT STOKES TRAVERSAL WITH ***
3314
+ ### //*** Alt+Shift+Up and Alt+Shift+Down when an ***
3315
+ ### //*** image with multiple stokes axes is available ***
3316
+ ### //****************************************************
3317
+ 'setup-key-mgmt': '''if ( source._hotkeys === undefined ) {
3318
+ source._hotkeys = { id: '%s' }
3319
+ // next channel -- all modes
3320
+ casalib.hotkeys( 'alt+up,ctrl+up,command+up', {scope: source._hotkeys.id},
3321
+ (e) => { e.preventDefault( )
3322
+ if ( source.cur_chan[1] + 1 >= source.num_chans[1] ) {
3323
+ // wrap round to the first channel
3324
+ source.channel( 0, source.cur_chan[0] )
3325
+ if ( chan_slider ) { chan_slider.value = 0 }
3326
+ } else {
3327
+ // advance to the next channel
3328
+ source.channel( source.cur_chan[1] + 1, source.cur_chan[0] )
3329
+ if ( chan_slider ) { chan_slider.value = source.cur_chan[1] + 1 }
3330
+ } } )
3331
+ // previous channel -- all modes
3332
+ casalib.hotkeys( 'alt+down,ctrl+down,command+down', { scope: source._hotkeys.id},
3333
+ (e) => { e.preventDefault( )
3334
+ if ( source.cur_chan[1] - 1 >= 0 ) {
3335
+ // advance to the prev channel
3336
+ source.channel( source.cur_chan[1] - 1, source.cur_chan[0] )
3337
+ if ( chan_slider ) { chan_slider.value = source.cur_chan[1] - 1 }
3338
+ } else {
3339
+ // wrap round to the last channel
3340
+ source.channel( source.num_chans[1] - 1, source.cur_chan[0] )
3341
+ if ( chan_slider ) { chan_slider.value = source.num_chans[1] - 1 }
3342
+ } } )
3343
+
3344
+ // next polarization/stokes -- all modes
3345
+ casalib.hotkeys( 'alt+right,ctrl+right,command+right', {scope: source._hotkeys.id},
3346
+ (e) => { e.preventDefault( )
3347
+ if ( source.cur_chan[0] + 1 >= source.num_chans[0] ) {
3348
+ // wrap round to the first channel
3349
+ source.channel( source.cur_chan[1], 0 )
3350
+ } else {
3351
+ // advance to the next channel
3352
+ source.channel( source.cur_chan[1], source.cur_chan[0] + 1 )
3353
+ } } )
3354
+ // previous polarization/stokes -- all modes
3355
+ casalib.hotkeys( 'alt+left,ctrl+left,command+left', { scope: source._hotkeys.id},
3356
+ (e) => { e.preventDefault( )
3357
+ if ( source.cur_chan[0] - 1 >= 0 ) {
3358
+ // advance to the prev channel
3359
+ source.channel( source.cur_chan[1], source.cur_chan[0] - 1 )
3360
+ } else {
3361
+ // wrap round to the last channel
3362
+ source.channel( source.cur_chan[1], source.num_chans[0] - 1)
3363
+ } } )
3364
+ %s
3365
+
3366
+ }''' % ( self._hotkey_state['id'],
3367
+ self._js_mode_code['no-bitmask-hotkey-setup'] if self._mask_path is None else
3368
+ self._js_mode_code['bitmask-hotkey-setup-add-sub'] + self._js_mode_code['bitmask-hotkey-setup'] )
3369
+ }
3370
+
3371
+
3372
+
3373
+ def __help_string( self, rows=[ ] ):
3374
+ '''Retrieve the help Bokeh object. When returned the ``visible`` property is
3375
+ set to ``False``, but it can be toggled based on GUI actions.
3376
+ '''
3377
+ #mask_control = { 'no-mask': '''
3378
+ # <tr><td><b>f</b></td><td>freeze cursor tracking updates until the mouse <b>re-enters</b> the channel plot</td></tr>
3379
+ # <tr><td><b>option</b></td><td>display mask cursor (<i>at least one mask must have been drawn</i>)</td></tr>
3380
+ # <tr><td><b>option</b>-<b>]</b></td><td>move cursor to next mask</td></tr>
3381
+ # <tr><td><b>option</b>-<b>[</b></td><td>move cursor to previous mask</td></tr>
3382
+ # <tr><td><b>option</b>-<b>/</b></td><td>add mask to selection set</td></tr>
3383
+ # <tr><td><b>option</b>-<b>escape</b></td><td>clear selection set</td></tr>
3384
+ # <tr><td><b>down</b></td><td>move selection set down one pixel</td></tr>
3385
+ # <tr><td><b>up</b></td><td>move selection set up one pixel</td></tr>
3386
+ # <tr><td><b>left</b></td><td>move selection one pixel to the left</td></tr>
3387
+ # <tr><td><b>right</b></td><td>move selection one pixel to the right</td></tr>
3388
+ # <tr><td><b>shift</b>-<b>up</b></td><td>move selection up several pixels</td></tr>
3389
+ # <tr><td><b>shift</b>-<b>down</b></td><td>move selection down several pixels</td></tr>
3390
+ # <tr><td><b>shift</b>-<b>left</b></td><td>move selection several pixels to the left</td></tr>
3391
+ # <tr><td><b>shift</b>-<b>right</b></td><td>move selection several pixels to the right</td></tr>
3392
+ # <tr><td><b>option</b>-<b>c</b></td><td>copy selection set to the copy buffer</td></tr>
3393
+ # <tr><td><b>option</b>-<b>v</b></td><td>paste selection set into the current channel</td></tr>
3394
+ # <tr><td><b>option</b>-<b>shift</b>-<b>v</b></td><td>paste selection set into all channels along the current stokes axis</td></tr>
3395
+ # <tr><td><b>option</b>-<b>delete</b></td><td>delete polygon indicated by the cursor</td></tr>''',
3396
+ # 'mask': '''
3397
+ # <tr><td><b>f</b></td><td>freeze cursor tracking updates until the mouse <b>re-enters</b> the channel plot</td></tr>
3398
+ # <tr><td><b>a</b></td><td>add region to the mask for the current channel</td></tr>
3399
+ # <tr><td><b>s</b></td><td>subtract region from the mask for the current channel</td></tr>
3400
+ # <tr><td><b>shift</b>-<b>a</b></td><td>add region to the mask for all channels</td></tr>
3401
+ # <tr><td><b>shift</b>-<b>s</b></td><td>subtract region from the mask for all channels</td></tr>
3402
+ # <tr><td><b>~</b></td><td>invert mask values for the current channel</td></tr>
3403
+ # <tr><td><b>!</b></td><td>invert mask values for all channels</td></tr>
3404
+ # <tr><td><b>escape</b></td><td>unselect the selected region</td></tr>
3405
+ # <tr><td><b>down</b></td><td>move selected region down one pixel</td></tr>
3406
+ # <tr><td><b>up</b></td><td>move selected region up one pixel</td></tr>
3407
+ # <tr><td><b>left</b></td><td>move selected region one pixel to the left</td></tr>
3408
+ # <tr><td><b>right</b></td><td>move selected region one pixel to the right</td></tr>
3409
+ # <tr><td><b>shift</b>-<b>up</b></td><td>move selected region up several pixels</td></tr>
3410
+ # <tr><td><b>shift</b>-<b>down</b></td><td>move selected region down several pixels</td></tr>
3411
+ # <tr><td><b>shift</b>-<b>left</b></td><td>move selected region several pixels to the left</td></tr>
3412
+ # <tr><td><b>shift</b>-<b>right</b></td><td>move selected region several pixels to the right</td></tr>'''
3413
+ # }
3414
+ mask_control = { 'no-mask': '''
3415
+ <tr><td><b>f</b></td><td>freeze cursor tracking updates until the mouse <b>re-enters</b> the channel plot</td></tr>
3416
+ <tr><td><b>option</b>-<b>delete</b></td><td>delete polygon indicated by the cursor</td></tr>''',
3417
+ 'mask': '''
3418
+ <tr><td><b>f</b></td><td>freeze cursor tracking updates until the mouse <b>re-enters</b> the channel plot</td></tr>
3419
+ <tr><td><b>a</b></td><td>add region to the mask for the current channel</td></tr>
3420
+ <tr><td><b>s</b></td><td>subtract region from the mask for the current channel</td></tr>
3421
+ <tr><td><b>shift</b>-<b>a</b></td><td>add region to the mask for all channels</td></tr>
3422
+ <tr><td><b>shift</b>-<b>s</b></td><td>subtract region from the mask for all channels</td></tr>
3423
+ <tr><td><b>~</b></td><td>invert mask values for the current channel</td></tr>
3424
+ <tr><td><b>!</b></td><td>invert mask values for all channels</td></tr>
3425
+ <tr><td><b>escape</b></td><td>unselect the selected region</td></tr>'''
3426
+ }
3427
+
3428
+ return \
3429
+ '''<html>
3430
+ <head>
3431
+ <title>Interactive Clean Help</title>
3432
+ <style>
3433
+ #makemaskhelp td, #makemaskhelp th {
3434
+ border: 1px solid #ddd;
3435
+ text-align: left;
3436
+ padding: 8px;
3437
+ }
3438
+ #makemaskhelp tr:nth-child(even){background-color: #f2f2f2}
3439
+ </style>
3440
+ </head>
3441
+ <body>
3442
+ <table id="makemaskhelp">
3443
+ <tr><th>buttons/key(s)</th><th>description</th></tr>
3444
+ EXTRAROWS
3445
+ <tr><td><b>option</b>-<b>up</b></td><td>to next channel (<b>ctrl</b> or <b>cmd</b> can be used)</td></tr>
3446
+ <tr><td><b>option</b>-<b>down</b></td><td>to previous channel (<b>ctrl</b> or <b>cmd</b> can be used)</td></tr>
3447
+ <tr><td><b>option</b>-<b>right</b></td><td>to next stokes axis (<b>ctrl</b> or <b>cmd</b> can be used)</td></tr>
3448
+ <tr><td><b>option</b>-<b>left</b></td><td>to previous stokes axis (<b>ctrl</b> or <b>cmd</b> can be used)</td></tr>
3449
+ MASKCONTROL
3450
+ </table>
3451
+ <hr>
3452
+ <p><b>This application was created using <a href="https://bokeh.org/"><svg width="55" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 490 140.19"><defs><style>.cls-1{fill:#010101;}.cls-2{fill:#23b355;}.cls-3{fill:#02abaf;}.cls-4{fill:#4998d3;}.cls-5{fill:#8a288a;}.cls-6{fill:#ed1557;}.cls-7{fill:#f05223;}.cls-8{fill:#f7aa1b;}.cls-9{fill:#a6ce38;}</style></defs><path class="cls-1" d="M16.53,0V52.2A35.25,35.25,0,0,1,30.3,40.85,42.2,42.2,0,0,1,48.37,37a46.58,46.58,0,0,1,23.76,6.08,42.92,42.92,0,0,1,16.44,17Q94.51,71,94.51,85.27t-5.94,25.29a42.77,42.77,0,0,1-16.44,17.07,46.58,46.58,0,0,1-23.76,6.08c-6.77,0-30.12-1.27-32.33-1.27s-16,.2-16,.2V2.53C.84,2.28,2.05,2,3.51,1.64a63.67,63.67,0,0,1,6.37-1c.64-.08,2.6-.32,5.18-.53ZM62.75,114.49a29.76,29.76,0,0,0,11-11.8,36.56,36.56,0,0,0,4-17.42,36.57,36.57,0,0,0-4-17.43,29.13,29.13,0,0,0-11-11.71A30.44,30.44,0,0,0,47,52a30.59,30.59,0,0,0-15.67,4.11,28.34,28.34,0,0,0-11,11.71,37.13,37.13,0,0,0-4,17.43,37.12,37.12,0,0,0,4,17.42,29,29,0,0,0,11,11.8A30.08,30.08,0,0,0,47,118.69,29.93,29.93,0,0,0,62.75,114.49Z"/><path class="cls-1" d="M390.12,73.66a45.29,45.29,0,0,0-4.66-13.39A43.84,43.84,0,0,0,376.11,48a42.68,42.68,0,0,0-6.69-5,44.51,44.51,0,0,0-23.37-6.17A45.41,45.41,0,0,0,322.5,43a43.46,43.46,0,0,0-16.39,17.25,51.58,51.58,0,0,0-5.95,24.84,50.89,50.89,0,0,0,6.12,25,43.2,43.2,0,0,0,17.34,17.24,56.37,56.37,0,0,0,46.32,2.24,40.17,40.17,0,0,0,15.44-11.44l-3.61-4.38h0a8.62,8.62,0,0,0-6.48-2.92,8.44,8.44,0,0,0-4.9,1.57l-.06,0q-8.55,6.08-20.48,6.07-13.45,0-22.51-7.6t-10.61-20.1h54.9a67.63,67.63,0,0,0,7.72-.39,14.15,14.15,0,0,0,6.57-3.4,14,14,0,0,0,4.36-7.5A15,15,0,0,0,390.12,73.66Zm-16.57-.35a6.85,6.85,0,0,1-2.75,4.13,6.28,6.28,0,0,1-2.48,1c-.26,0-3.57.06-3.57.06h-48q1.38-12.15,9.4-19.65t19.93-7.51a29.34,29.34,0,0,1,13.54,3.06A28.9,28.9,0,0,1,366,58.93a27.77,27.77,0,0,1,5.45,6.94C373,68.76,374.11,70.81,373.55,73.31Z"/><path class="cls-1" d="M213,2.53c.84-.25,2.05-.58,3.51-.89a63.67,63.67,0,0,1,6.37-1c.64-.08,2.6-.32,5.18-.53L229.54,0V85.09L279.29,37.9h20L260.87,77,303,132.64H282.73L248.47,88.48S232.52,102,230.81,106.77a18.9,18.9,0,0,0-1.27,6.37c0,8,0,19.5,0,19.5H213Z"/><path class="cls-1" d="M403.41,2.53c.84-.25,2-.58,3.51-.89a63.21,63.21,0,0,1,6.37-1c.64-.08,2.6-.32,5.18-.53L419.93,0V51.3a34.26,34.26,0,0,1,13.52-10.54A44.91,44.91,0,0,1,452.13,37q17.39,0,27.63,10.46T490,78.12v54.52H473.47V80.08q0-13.76-6.37-20.73t-18.24-7q-13.44,0-21.18,8.13t-7.75,23.33v48.8H403.41Z"/><path class="cls-2" d="M171.65,43A67.84,67.84,0,0,0,167,37.57c-5.1-5.42-7.82-6.49-9.43-6.82a12.85,12.85,0,0,0-3-.22c-4.3.2-7.26,2.48-9.86,4.48a31.93,31.93,0,0,0-6.79,7.21.42.42,0,0,0-.07.24V68.61a.43.43,0,0,0,.23.38.41.41,0,0,0,.19,0,.44.44,0,0,0,.25-.08l33.06-25.32A.42.42,0,0,0,171.65,43Z"/><path class="cls-3" d="M195.07,49.47a11.91,11.91,0,0,0-2-2.3c-3.18-2.9-6.88-3.38-10.14-3.81a31.64,31.64,0,0,0-9.89.3.43.43,0,0,0-.22.12l-18.5,18.49a.43.43,0,0,0-.1.43.43.43,0,0,0,.34.28l41.28,5.48h.06a.4.4,0,0,0,.41-.36,66.5,66.5,0,0,0,.6-7.15C197.15,53.51,196,50.83,195.07,49.47Z"/><path class="cls-4" d="M208.56,86.17c-.2-4.3-2.47-7.25-4.48-9.86a31.46,31.46,0,0,0-7.21-6.78.4.4,0,0,0-.23-.08H170.48a.4.4,0,0,0-.37.24.38.38,0,0,0,0,.43l25.32,33.07a.42.42,0,0,0,.33.16.38.38,0,0,0,.25-.09,69.78,69.78,0,0,0,5.48-4.63c5.42-5.1,6.48-7.82,6.82-9.42A13.2,13.2,0,0,0,208.56,86.17Z"/><path class="cls-5" d="M195.43,104.66a.39.39,0,0,0-.11-.22L176.82,86a.39.39,0,0,0-.43-.1.41.41,0,0,0-.28.34l-5.47,41.28a.42.42,0,0,0,.36.47,69.17,69.17,0,0,0,7.14.59l1.81,0c6.08,0,8.43-1,9.68-1.87a12.71,12.71,0,0,0,2.3-2c2.9-3.19,3.38-6.88,3.8-10.14A31.7,31.7,0,0,0,195.43,104.66Z"/><path class="cls-6" d="M169.41,101.72a.44.44,0,0,0-.44,0l-33.06,25.32a.43.43,0,0,0-.08.59,69.62,69.62,0,0,0,4.63,5.47c5.1,5.42,7.83,6.49,9.43,6.82a12.47,12.47,0,0,0,2.42.23h.61c4.31-.2,7.26-2.48,9.86-4.48a31.71,31.71,0,0,0,6.79-7.21.42.42,0,0,0,.07-.24V102.09A.41.41,0,0,0,169.41,101.72Z"/><path class="cls-7" d="M153.25,108a.4.4,0,0,0-.34-.28l-41.28-5.48a.42.42,0,0,0-.47.36c-.32,2.38-.52,4.78-.6,7.14-.22,7.44,1,10.12,1.85,11.49a12.3,12.3,0,0,0,2,2.3c3.18,2.9,6.88,3.38,10.14,3.81a30.93,30.93,0,0,0,4,.25,32.21,32.21,0,0,0,5.92-.55.45.45,0,0,0,.21-.12l18.5-18.49A.43.43,0,0,0,153.25,108Z"/><path class="cls-8" d="M137.33,100.58,112,67.52a.44.44,0,0,0-.59-.08A70.07,70.07,0,0,0,106,72.08c-5.43,5.1-6.49,7.82-6.82,9.42a12.46,12.46,0,0,0-.22,3c.2,4.3,2.47,7.25,4.48,9.86a31.46,31.46,0,0,0,7.21,6.78.41.41,0,0,0,.24.08H137a.42.42,0,0,0,.33-.68Z"/><path class="cls-9" d="M136.49,42.77a69.36,69.36,0,0,0-7.15-.59c-7.44-.23-10.12.94-11.49,1.84a12.71,12.71,0,0,0-2.3,2c-2.9,3.18-3.38,6.88-3.8,10.14a31.7,31.7,0,0,0,.3,9.9.57.57,0,0,0,.11.22l18.5,18.49a.43.43,0,0,0,.3.13.31.31,0,0,0,.13,0,.41.41,0,0,0,.28-.34l5.47-41.28A.41.41,0,0,0,136.49,42.77Z"/></svg></b></a>
3453
+ </body>
3454
+ </html>'''.replace('option','option' if platform == 'darwin' else 'alt') \
3455
+ .replace('<b>delete</b>','<b>delete</b>' if platform == 'darwin' else '<b>backspace</b>') \
3456
+ .replace('EXTRAROWS','\n'.join(rows)) \
3457
+ .replace('MASKCONTROL', mask_control['no-mask'] if self._mask_path is None else mask_control['mask'])