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