GUI

The Python syntax for defining a new class that is derived from an existing class uses a class statement. This is the statement that defines the RegionBox class:

class RegionBox(pn.Column):
    ...

pn.Column is an existing widget class, defined in the Panel library. That means our new RegionBox objects will be special types of columns that can be inserted into the GUI at some place.

The code that is called to create a new object is a function named __init__ defined inside the class. The first argument to __init__ is always self, which is a reference to the object being built.

Here is a simplified version of the __init__ function for the RegionBox class (the actual definition is shown below, in the documentation for RegionBox):

class RegionBox(pn.Column):

    def __init__(self, project):
        self.boxes = { }
        for name in OP.region_names:
            box = pn.widgets.Checkbox(name=name, styles=box_styles, stylesheets=[box_style_sheet])
            box.param.watch(self.cb, ['value'])
            self.boxes[name] = box

When this function is called, it initializes a variable named boxes to be an empty dictionary. The for loop iterates over all the region names. It makes a Checkbox widget for each region and adds the box to the dictionary.

The line in the middle of the loop that calls box.param.watch is where all the "magic" happens. This function call tells the GUI that whenever a checkbox is clicked it should call a function named cb that is also defined inside the RegionBox class. Here is a simplified version:

def cb(self, event):
    r = event.obj.name
    if event.new:
        self.selected.add(r)
    else:
        self.selected.remove(r)

The name cb is short for "callback", a common name for this type of function. The parameter named event has information about what the user just did. In this case, we want to get the name of the button (which will be one of the region names) and then update the set of selected regions. If the button was turned on we add the region name to the set, otherwise we remove it.

TideGatesApp

Bases: BootstrapTemplate

The web application is based on the Bootstrap template provided by Panel. It displays a map (an instance of the TGMap class) in the sidebar. The main content area has a Tabs widget with five tabs: a welcome message, a help page, the main page (described below) and two tabs for displaying outputs.

The application also displays several small help buttons next to the main widgets. Clicking one of these buttons brings up a floating window with information about the widget.

The main tab (labeled "Start") displays the widgets that allow the user to specify optimization parameters: region names, budget levels, and restoration targets. It also has a Run button. When the user clicks this button the callback function makes sure the necessary parameters have been defined and then uses the template's modal dialog area. Clicking the "OK" button in that dialog invokes another callback, defined here, that runs the optimizer.

Parameters:
  • params

    runtime options passed to the parent class constructor

Source code in src/gui/app.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def __init__(self, **params):
    """
    Initialize the application.

    Arguments:
      params:  runtime options passed to the parent class constructor
    """
    super(TideGatesApp, self).__init__(**params)

    # self.project_menu = pn.widgets.Select(options=['Demo','Oregon'], width=150)
    # self.header.append(pn.Row(
    #     pn.layout.HSpacer(),
    #     self.project_menu
    # ))

    self.map = TGMap.init()
    self.map_pane = pn.Column(
        pn.panel(self.map.graphic())
    )

    self.budget_box = BudgetBox()
    self.region_boxes = RegionBox(self.map, self.budget_box)
    self.target_boxes = TargetBox()

    self.optimize_button = pn.widgets.Button(name='Run Optimizer', stylesheets=[button_style_sheet])

    self.info = InfoBox(self, self.run_cb)
    self.modal.append(self.info)

    self.map_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.map_help_button.on_click(self.map_help_cb)

    self.region_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.region_help_button.on_click(self.region_help_cb)

    self.budget_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.budget_help_button.on_click(self.budget_help_cb)

    self.target_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.target_help_button.on_click(self.target_help_cb)

    self.climate_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.climate_help_button.on_click(self.climate_help_cb)

    # self.tab_height = int(self.map.graphic().height * 1.05)
    self.tab_height = 900

    welcome_tab = pn.Column(
        self.section_head('Welcome'),
        pn.pane.HTML(OP.fetch_html_file('welcome.html')),
        height = self.tab_height,
        scroll = True,
    )

    help_tab = pn.Column(
        self.section_head('Instructions'),
        pn.pane.HTML(OP.fetch_html_file('help.html')),
        height = self.tab_height,
        scroll = True,
     )

    start_tab = pn.Column(
        self.section_head('Geographic Regions', self.region_help_button),
        self.region_boxes,

        self.section_head('Budgets', self.budget_help_button),
        self.budget_box,

        self.section_head('Targets', self.target_help_button),
        self.target_boxes,

        self.optimize_button,
    )

    output_tab = pn.Column(
        self.section_head('Nothing to See Yet'),
        pn.pane.HTML('<p>After running the optimizer this tab will show the results.</p>')
    )

    download_tab = pn.Column(
        self.section_head('Nothing to Download Yet'),
        pn.pane.HTML('<p>After running the optimizer use this tab to save the results.</p>')        )

    self.tabs = pn.Tabs(
        ('Home', welcome_tab),
        ('Help', help_tab),
        ('Start', start_tab),
        ('Output', output_tab),
        ('Download', download_tab),
        stylesheets=[tab_style_sheet],
        # tabs_location='left',
        # sizing_mode = 'fixed',
        # width=800,
        # height=700,
    )

    self.sidebar.append(pn.Row(self.map_pane, self.map_help_button))
    self.main.append(self.tabs)

    self.optimize_button.on_click(self.validate_settings)

    for r in DevOP.default_regions():
        self.region_boxes.check(r)
    self.budget_box.set_value(DevOP.default_budget())
    self.target_boxes.set_selection(DevOP.default_targets())
    if DevOP.results_dir():
        self.run_optimizer()
    self.tabs.active = OP.initial_tab

section_head(s, b=None)

Create an HTML header for one of the sections in the Start tab.

Source code in src/gui/app.py
148
149
150
151
152
153
def section_head(self, s, b = None):
    """
    Create an HTML header for one of the sections in the Start tab.
    """
    header = pn.pane.HTML(f'<h3>{s}</h3>', styles=header_styles)
    return header if b is None else pn.Row(header, b)

validate_settings(_)

Callback function invoked when the user clicks the Run Optimizer button.

Source code in src/gui/app.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def validate_settings(self, _):
    """
    Callback function invoked when the user clicks the Run Optimizer button.
    """
    regions = self.region_boxes.selection()
    budget = self.budget_box.defined()
    targets = self.target_boxes.selection()

    if len(regions) == 0 or (not budget) or len(targets) == 0:
        self.info.show_missing(regions, budget, targets)
        return

    if weights := self.target_boxes.weights():
        if not all([w.isdigit() and (1 <= int(w) <= 5) for w in weights]):
            self.info.show_invalid_weights(weights)
            return

    mapping = self.target_boxes.mapping()

    self.info.show_params(regions, self.budget_box.values(), targets, weights, mapping)

run_cb(_)

Callback function invoked when the user clicks the Continue button after verifying the parameter options.

Wrap the call to the function that runs the optimizer in code that shows the loading icon and opens a message when the function returns.

Source code in src/gui/app.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def run_cb(self, _):
    """
    Callback function invoked when the user clicks the Continue button after verifying
    the parameter options.

    Wrap the call to the function that runs the optimizer in code that shows the loading
    icon and opens a message when the function returns.
    """
    try:
        self.close_modal()
        self.main[0].loading = True
        self.run_optimizer()
        self.main[0].loading = False
        self.info.show_success()
    except OPServerError as err:
        self.main[0].loading = False
        self.info.show_fail(err)

run_optimizer()

Use the settings in the widgets to run OptiPass, save the results in the output tab.

Source code in src/gui/app.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def run_optimizer(self):
    """
    Use the settings in the widgets to run OptiPass, save the results
    in the output tab.
    """
    params = [
        self.region_boxes.selection(),
        self.budget_box.values(),
        self.target_boxes.selection(),
        self.target_boxes.weights(),
        self.target_boxes.mapping(),
    ]

    resp = OP.run_optimizer(*params)

    params += resp
    res = OPResult(*params)
    output = OutputPane(res, self.map)

    self.region_boxes.add_external_callback(output.hide_dots)
    self.tabs[3] = ('Output', pn.Column(output, height=self.tab_height, scroll=True))
    self.tabs[4] = ('Download', pn.Column(DownloadPane(res), height=self.tab_height, scroll=True))

map_help_cb(_)

Callback function for the help button next to the map in the sidebar.

Source code in src/gui/app.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def map_help_cb(self, _):
    """
    Callback function for the help button next to the map in the sidebar.
    """
    msg = pn.pane.HTML('''
    <p>When you move your mouse over the map the cursor will change to a "crosshairs" symbol and a set of buttons will appear below the map.
    Navigating with the map is similar to using Google maps or other online maps:</p>
    <ul>
        <li>Left-click and drag to pan (move left and right or up and down).</li>
        <li>If you want to zoom in and out, first click the magnifying glass button below the map; then you can zoom in and out using the scroll wheel on your mouse.</li>   
        <li>Click the refresh button to restore the map to its original size and location.</li>
    </ul>
    ''')
    self.tabs[0].append(pn.layout.FloatPanel(msg, name='Map Controls', contained=False, position='center', width=400))

region_help_cb(_)

Callback function for the help button next to the region box widget in the start tab.

Source code in src/gui/app.py
232
233
234
235
236
237
238
239
240
241
def region_help_cb(self, _):
    """
    Callback function for the help button next to the region box widget in the start tab.
    """
    msg = pn.pane.HTML('''
    <p>Select a region by clicking in the box to the left of an estuary name.</p>
    <p>Each time you click in a box the map will be updated to show the positions of the barriers that are in our database for the estuary.</p>
    <p>You must select at least one region before you run the optimizer.</p>
    ''')
    self.tabs[2].append(pn.layout.FloatPanel(msg, name='Geographic Regions', contained=False, position='center', width=400))

budget_help_cb(_)

Callback function for the help button next to the budget box widget in the start tab.

Source code in src/gui/app.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def budget_help_cb(self, _):
    """
    Callback function for the help button next to the budget box widget in the start tab.
    """
    msg = pn.pane.HTML('''
    <p>There are three ways to specify the budgets used by the optimizer.</p>
    <H4>Basic</H4>
    <p>The simplest method is to specify an upper limit by moving the slider back and forth.  When you use this method, the optimizer will run 10 times, ending at the value you select with the slider.  For example, if you set the slider at $10M (the abbreviation for $10 million), the optimizer will make ROI curves based on budgets of $1M, $2M, <i>etc</i>, up to the maximum of $10M.</p>
    <p>Note that the slider is disabled until you select one or more regions.  That's because the maximum value depends on the costs of the gates in each region.
    For example, the total cost of all gates in the Coquille region is $11.8M.  Once you choose that region, you can move the budget slider
    left and right to pick a maximum budget for the optimizer to consider.
    <H4>Advanced</H4>
    <p>If you click on the Advanced tab in this section you will see ways to specify the budget interval and the number of budgets.</p>
    <p>You can use this method if you want more control over the layout of the ROI curves, for example you can include more points by increasing the number of budgets.</p>
    <H4>Fixed</H4>
    <p>If you know exactly how much money you have to spend you can enter that amount by clicking on the Fixed tab and entering the budget amount.</p>
    <p>The optimizer will run just once, using that budget.  The output will have tables showing the gates identified by the optimizer, but there will be no ROI curve.</p>
    <p>When entering values, you can write the full amount, with or without commas (<i>e.g.</i>11,500,000 or 11500000) or use the abbreviated form (11.5M).</p>
    ''')
    self.tabs[2].append(pn.layout.FloatPanel(msg, name='Budget Levels', contained=False, position='center', width=400))

target_help_cb(_)

Callback function for the help button next to the target box widget in the start tab.

Source code in src/gui/app.py
264
265
266
267
268
269
270
271
272
273
def target_help_cb(self, _):
    """
    Callback function for the help button next to the target box widget in the start tab.
    """
    msg = pn.pane.HTML('''
    <p>Click boxes next to one or more target names to have the optimizer include those targets in its calculations.</p>
    <p>The optimizer will create an ROI curve for each target selected. </p>
    <p>If more than one target is selected the optimizer will also generate an overall "net benefit" curve based on considering all targets at the same time.</p>
    ''')
    self.tabs[2].append(pn.layout.FloatPanel(msg, name='Targets', contained=False, position='center', width=400))

climate_help_cb(_)

Callback function for the help button next to the climate scenario checkbox in the start tab.

Source code in src/gui/app.py
275
276
277
278
279
280
281
282
283
def climate_help_cb(self, _):
    """
    Callback function for the help button next to the climate scenario checkbox in the start tab.
    """
    msg = pn.pane.HTML('''
    <p>By default the optimizer uses current water levels when computing potential benefits.  Click the button next to <b>Future</b> to have it use water levels expected due to climate change.</p>
    <p>The future scenario uses two projected water levels, both for the period to 2100. For fish habitat targets, the future water level is based on projected sea level rise of 5.0 feet.  For agriculture and infrastructure targets, the future water level is projected to be 7.2 feet, which includes sea level rise and the probabilities of extreme water levels causing flooding events.</p>
    ''')
    self.tabs[2].append(pn.layout.FloatPanel(msg, name='Targets', contained=False, position='center', width=400))

BudgetBox

Bases: Column

There are three ways users can specify the range of budget values when running OptiPass. A BudgetBox widget has one tab for each option. The widgets displayed inside a tab are defined by their own classes (BasicBudgetBox, AdvancedBudgetBox, and FixedBudgetBox).

Source code in src/gui/budgets.py
19
20
21
22
23
24
25
26
def __init__(self):
    super(BudgetBox, self).__init__()
    self.tabs = pn.Tabs(
        ('Basic', BasicBudgetBox()),
        ('Advanced', AdvancedBudgetBox()),
        ('Fixed', FixedBudgetBox()),
    )
    self.append(self.tabs)

set_budget_max(n)

When the user selects or deselects a region the budget widgets need to know the new total cost for all the selected regions. This method passes that information to each of the budget widgets.

Parameters:
  • n (int) –

    the new maximum budget amount

Source code in src/gui/budgets.py
28
29
30
31
32
33
34
35
36
37
38
def set_budget_max(self, n: int):
    """
    When the user selects or deselects a region the budget widgets need
    to know the new total cost for all the selected regions.  This method
    passes that information to each of the budget widgets.

    Arguments:
      n: the new maximum budget amount
    """
    for t in self.tabs:
        t.set_budget_max(n)

values()

Return the budget settings for the currently selected budget type. Get the widget values from the active budget type, convert them into a tuple of values that will be passed to the optimizer.

Returns:
  • bmin

    the starting budget

  • binc

    the increment between budget values

  • bcnt

    the number of budget values

Source code in src/gui/budgets.py
40
41
42
43
44
45
46
47
48
49
50
51
def values(self):
    """
    Return the budget settings for the currently selected budget type.  Get
    the widget values from the active budget type, convert them into a tuple
    of values that will be passed to the optimizer.

    Returns:
      bmin:  the starting budget
      binc:  the increment between budget values
      bcnt:  the number of budget values 
    """
    return self.tabs[self.tabs.active].values()

defined()

Return True if the user has defined a budget using the current tab

Source code in src/gui/budgets.py
53
54
55
56
57
def defined(self):
    """
    Return True if the user has defined a budget using the current tab
    """
    return self.tabs[self.tabs.active].defined()

set_value(n)

Initialize the GUI by setting an initial budget value

Source code in src/gui/budgets.py
59
60
61
62
63
def set_value(self, n):
    """
    Initialize the GUI by setting an initial budget value
    """
    self.tabs[self.tabs.active].set_value(n)

BasicBudgetBox

Bases: WidgetBox

The default budget widget displays a slider that ranges from 0 up to a maximum value based on the total cost of all barriers in currently selected regions.

Source code in src/gui/budgets.py
87
88
89
90
91
92
93
94
95
96
97
98
def __init__(self):
    super(BasicBudgetBox, self).__init__(margin=(15,0,15,5))
    self.labels = [ x[0] for x in self.levels ]
    self.map = { x[0]: x[1] for x in self.levels }
    self.slider = pn.widgets.DiscreteSlider(
        options = self.labels[:self.MIN_LEVELS], 
        value = self.labels[0],
        name = 'Maximum Budget',
        margin=(20,20,20,20),
        stylesheets=[slider_style_sheet],
    )
    self.append(self.slider)

set_budget_max(n)

Choose a maximum budget by scanning a table of budget levels to find the first one less than the total cost.

Parameters:
  • n

    the total cost of all barriers in the current selection.

Source code in src/gui/budgets.py
100
101
102
103
104
105
106
107
108
109
110
111
112
def set_budget_max(self, n):
    """
    Choose a maximum budget by scanning a table of budget levels to
    find the first one less than the total cost.

    Arguments:
      n: the total cost of all barriers in the current selection.
    """
    for i in range(len(self.levels)-1, -1, -1):
        if n >= self.levels[i][1]:
            break
    i = max(i, self.MIN_LEVELS)
    self.slider.options = self.labels[:i+1]

values()

The basic budget always has the same number of budgets and always starts with $0. Determine the increment by dividing the max budget in the slider by the number of budgets.

Source code in src/gui/budgets.py
116
117
118
119
120
121
122
123
def values(self):
    """
    The basic budget always has the same number of budgets and always
    starts with $0.  Determine the increment by dividing the max budget
    in the slider by the number of budgets.
    """
    x = self.map[self.slider.value]
    return 0, x // self.BUDGET_COUNT, self.BUDGET_COUNT

defined()

The basic budget is set if the slider is not in the first location.

Source code in src/gui/budgets.py
125
126
127
128
129
def defined(self):
    """
    The basic budget is set if the slider is not in the first location.
    """
    return self.slider.value != '$0'

set_value(n)

Set the slider to n

Source code in src/gui/budgets.py
131
132
133
134
135
def set_value(self, n):
    """
    Set the slider to n
    """
    self.slider.value = self.slider.options[n]

FixedBudgetBox

Bases: WidgetBox

This option is for situations where a user knows exactly how much money they have to spend and want to know the optimal set of barriers to replace for that amount of money. OptiPass is run twice -- once to determine the current passabilities, and once to compute the benefit from the specified budget. The widget simply displays a box where the user enters the dollar amount for their budget.

Source code in src/gui/budgets.py
147
148
149
150
def __init__(self):
    super(FixedBudgetBox, self).__init__(margin=(15,0,15,5))
    self.input = pn.widgets.TextInput(name='Budget Amount', value='$')
    self.append(self.input)

values()

A fixed budget has one value, returned as the starting budget. The the increment is 0 and count is 1.

Source code in src/gui/budgets.py
155
156
157
158
159
160
161
162
163
164
def values(self):
    """
    A fixed budget has one value, returned as the starting budget.  The 
    the increment is 0 and count is 1.
    """
    s = self.input.value
    if s.startswith('$'):
        s = s[1:]
    n = self.parse_dollar_amount(self.input.value)
    return n, 0, 1

defined()

The fixed budget is set if the text box is not empty.

Source code in src/gui/budgets.py
166
167
168
169
170
def defined(self):
    """
    The fixed budget is set if the text box is not empty.
    """
    return self.parse_dollar_amount(self.input.value) > 0

set_value(n)

Initialize the budget to n

Source code in src/gui/budgets.py
172
173
174
175
176
def set_value(self, n):
    """
    Initialize the budget to n
    """
    self.input.value = f'${n}'

parse_dollar_amount(s)

Make sure the string entered by the user has an acceptable format. It can be all digits (e.g. "1500000"), or digits separated by commas (e.g. "1,500,000"), or a number followed by a K or M (e.g. "1.5M"). There can be a dollar sign at the front of the string.

Parameters:
  • s (str) –

    the string entered into the text box

Returns:
  • the value of the string converted into an integer

Source code in src/gui/budgets.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def parse_dollar_amount(self, s: str):
    """
    Make sure the string entered by the user has an acceptable format.
    It can be all digits (e.g. "1500000"), or digits separated by commas
    (e.g. "1,500,000"), or a number followed by a K or M (e.g. "1.5M").
    There can be a dollar sign at the front of the string.

    Arguments:
      s:  the string entered into the text box

    Returns:
      the value of the string converted into an integer

    """
    try:
        if s.startswith('$'):
            s = s[1:]
        if s.endswith(('K','M')):
            multiplier = 1000 if s.endswith('K') else 1000000
            res = int(float(s[:-1]) * multiplier)
        elif ',' in s:
            parts = s.split(',')
            assert len(parts[0]) <= 3 and (len(parts) == 1 or all(len(p) == 3 for p in parts[1:]))
            res = int(''.join(parts))
        else:
            res = 0 if s == '' else int(s)
        return res
    except Exception:
        return 0

AdvancedBudgetBox

Bases: WidgetBox

The "advanced" option gives the user the most control over the budget values processed by OptiPass by letting them specify the number of budget levels (in the basic budget there are always 10 budget levels).

This box has three widgets: a slider to specify the maximum amount, another slider to specify the increment between budgets, and an input box to specify the number of budgets. Adjusting the value of any of these widgets automatically updates the other two. For example, if the maximum is set to $1M and the number of budgets is 10, the increment is $100K. If the user changes the number of budgets to 20, the increment drops to $50K. Or if they change the maximum to $2M, the increment increases to $200K.

Source code in src/gui/budgets.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def __init__(self):
    super(AdvancedBudgetBox, self).__init__(margin=(15,0,15,5), width=self.BOX_WIDTH)

    self.cap = 0

    self.max_slider = pn.widgets.FloatSlider(
        name='Maximum Budget', 
        start=0, 
        end=1, 
        step=self.MAX_STEP,
        value=0,
        width=self.MAX_SLIDER_WIDTH,
        format=NumeralTickFormatter(format='$0,0'),
        stylesheets=[slider_style_sheet],
    )

    self.inc_slider = pn.widgets.FloatSlider(
        name='Budget Interval', 
        start=0, 
        end=1, 
        step=self.INC_STEP,
        value=0,
        width=self.INC_SLIDER_WIDTH,
        format=NumeralTickFormatter(format='$0,0'),
        stylesheets=[slider_style_sheet],
    )

    self.count_input = pn.widgets.IntInput(
        value=10, 
        step=1, 
        start=self.COUNT_MIN,
        end=self.COUNT_MAX,
        width=75,
    )

    self.append(pn.GridBox(
        nrows=2,
        ncols=2,
        objects=[
            self.max_slider,
            self.inc_slider,
            pn.pane.HTML('<b>Limit: N/A<b>'),
            pn.Row(pn.pane.HTML('#Budgets:'),self.count_input, align=('start','center'))
        ]
    ))

    self.max_slider.param.watch(self.max_updated, ['value'])
    self.inc_slider.param.watch(self.inc_updated, ['value'])
    self.count_input.param.watch(self.count_updated, ['value'])

values()

In this widget the budget increment and budget count are determined by the values in the corresponding widgets.

Source code in src/gui/budgets.py
281
282
283
284
285
286
def values(self):
    """
    In this widget the budget increment and budget count are determined
    by the values in the corresponding widgets.
    """
    return 0, self.inc_slider.value, self.count_input.value

defined()

The advance budget is set if the increment is not 0

Source code in src/gui/budgets.py
288
289
290
291
292
def defined(self):
    """
    The advance budget is set if the increment is not 0
    """
    return self.inc_slider.value > 0

set_value(n)

Set the budget to n

Source code in src/gui/budgets.py
294
295
296
297
298
def set_value(self, n):
    """
    Set the budget to n
    """
    self.max_slider.value = n

set_budget_max(n)

Called when the user selects or deselects a region. Save the new maximum, and update the value of the increment based on the new maximum.

Parameters:
  • n

    the total cost of all barriers in the selected regions.

Source code in src/gui/budgets.py
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def set_budget_max(self, n):
    """
    Called when the user selects or deselects a region.  Save the new
    maximum, and update the value of the increment based on the new maximum.

    Arguments:
      n:  the total cost of all barriers in the selected regions.
    """
    self.max_slider.end = max(1, n)
    self.max_slider.start = self.MAX_STEP
    self.inc_slider.end = max(1, n // 2)
    self.inc_slider.start = max(self.INC_STEP, n / self.COUNT_MAX)
    lim = 'N/A' if n == 0 else f'${n/1000000:.2f}M'
    self.objects[0][2] = pn.pane.HTML(f'<b>Limit: {lim}</b>')

max_updated(e)

Callback function invoked when the user moves the maximum budget slider. Computes a new budget increment.

Source code in src/gui/budgets.py
315
316
317
318
319
320
321
322
323
def max_updated(self, e):
    """
    Callback function invoked when the user moves the maximum budget
    slider.  Computes a new budget increment.
    """
    try:
        self.inc_slider.value = self.max_slider.value // self.count_input.value
    except ArithmeticError:
        pass

inc_updated(e)

Callback function invoked when the user changes the budget increment. Computes a new number of budgets.

Source code in src/gui/budgets.py
325
326
327
328
329
330
331
332
333
334
335
def inc_updated(self, e):
    """
    Callback function invoked when the user changes the budget increment.
    Computes a new number of budgets.
    """
    try:
        c = max(self.COUNT_MIN, self.max_slider.value // self.inc_slider.value)
        c = min(self.COUNT_MAX, c)
        self.count_input.value = c
    except ArithmeticError:
        pass

count_updated(e)

Callback function invoked when the user changes the number of budget levels. Computes a new budget increment.

Source code in src/gui/budgets.py
337
338
339
340
341
342
343
344
345
def count_updated(self, e):
    """
    Callback function invoked when the user changes the number of budget
    levels.  Computes a new budget increment.
    """
    try:
        self.inc_slider.value = self.max_slider.value // self.count_input.value
    except ArithmeticError:
        pass

DownloadPane

Bases: Column

After OptiPass has completed the last optimization run the GUI creates an instance of this class and saves it in the Download tab of the top level display.

Check the output panel to see which plots were created and to enable the net benefit plot if there is one.

The pane also has a form to allow the user to enter the name of the download file, the format for the figures, and a button to click when they are ready to download the data.

Parameters:
  • op

    the OPResult object containing data tables and plots

Source code in src/gui/output.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def __init__(self, op):
    """
    Display a set of checkboxes for the user to select what sort of data to
    include in a zip file.  If the gate table is not empty enable table downloads.
    Check the output panel to see which plots were created and to enable the
    net benefit plot if there is one.

    The pane also has a form to allow the user to enter the name of the download
    file, the format for the figures, and a button to click when they are ready
    to download the data.

    Arguments:
      op:  the OPResult object containing data tables and plots
    """
    super(DownloadPane, self).__init__()
    self.op = op
    self.folder_name = self._make_folder_name()

    self.grid = pn.GridBox(ncols=2)
    self.boxes = { }
    for x in [self.NB, self.BS, self.IT, self.BD]:
        b = pn.widgets.Checkbox(name=x, styles=box_styles, stylesheets=[box_style_sheet])
        if x in [self.NB, self.IT]:
            b.disabled = True
            b.value = False
        else:
            b.value = True
        self.boxes[x] = b
        self.grid.objects.append(b)

    self.filename_input = pn.widgets.TextInput(
        name = '', 
        value = self.folder_name,
    )

    self.image_type = pn.widgets.RadioBoxGroup(name='IFF', options=['HTML','PDF','PNG','JPEG'], inline=True)

    self.make_archive_button = pn.widgets.Button(name='Create Output Folder', stylesheets=[button_style_sheet])
    self.make_archive_button.on_click(self._archive_cb)

    self.append(pn.pane.HTML('<h3>Save Outputs</h3>', styles=header_styles))
    if len(self.op.matrix) > 0:
        self.append(pn.pane.HTML('<b>Items to Include in the Output Folder:</b>')),
        self.append(self.grid)
        self.append(pn.Row(
            pn.pane.HTML('<b>Image File Format:</b>'),
            self.image_type,
            margin=(20,0,0,0),
        ))
        self.append(pn.Row(
            pn.pane.HTML('<b>Output Folder Name:</b>'),
            self.filename_input,
            margin=(20,0,0,0),
        ))
        self.append(self.make_archive_button)
        self.append(pn.pane.HTML('<p>placeholder</p>', visible=False))

    # if there are figures at least one of them is an individual target, so enable
    # that option; if there is a net benefit figure it's the first figure, enable it
    # if it's there

    if len(self.op.display_figures) > 0:
        if self.op.display_figures[0][0] == 'Net':
            self.boxes[self.NB].value = True
            self.boxes[self.NB].disabled = False
        self.boxes[self.IT].value = True
        self.boxes[self.IT].disabled = False

_make_folder_name()

Use the region names, target names, and budget range to create the default name of the zip file.

Source code in src/gui/output.py
263
264
265
266
267
268
269
270
271
272
273
274
275
def _make_folder_name(self):
    """
    Use the region names, target names, and budget range to create the default name of the zip file.
    """
    parts = [s[:3] for s in self.op.regions]
    lst = self.op.targets
    if self.op.weights:
        lst = [f'{lst[i]}x{self.op.weights[i]}' for i in range(len(lst))]
    parts.extend(lst)
    parts.append(OP.format_budget_amount(self.op.binc*self.op.bcount)[1:])
    if self.op.mapping:
        parts.append(self.op.mapping)
    return '_'.join(parts)

_archive_cb(e)

Function called when the user clicks the Download button. Create the output folder and compress it. When the archive is ready, display a FileDownload widget with a button that starts the download.

Source code in src/gui/output.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def _archive_cb(self, e):
    """
    Function called when the user clicks the Download button.  Create the output
    folder and compress it.  When the archive is ready, display a FileDownload
    widget with a button that starts the download.
    """
    if not any([x.value for x in self.boxes.values()]):
        return
    self.loading = True
    base = self._make_archive_dir()
    self._save_files(base)
    p = make_archive(base, 'zip', base)
    self.loading = False
    self[-1] = pn.widgets.FileDownload(file=p, filename=self.filename+'.zip', stylesheets=[button_style_sheet])

_make_archive_dir()

Create an empty directory for the download, using the name in the form.

Source code in src/gui/output.py
292
293
294
295
296
297
298
299
300
301
def _make_archive_dir(self):
    """
    Create an empty directory for the download, using the name in the form.
    """
    self.filename = self.filename_input.value_input or self.filename_input.value
    archive_dir = Path.cwd() / 'tmp' / self.filename
    if Path.exists(archive_dir):
        rmtree(archive_dir)
    Path.mkdir(archive_dir)
    return archive_dir

_save_files(loc)

Write the tables and figures to the download directory.

Parameters:
  • loc

    the path to the directory.

Source code in src/gui/output.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
def _save_files(self, loc):
    """
    Write the tables and figures to the download directory.

    Arguments:
      loc:  the path to the directory.
    """
    figures = self.op.display_figures if self.image_type.value == 'HTML' else self.op.download_figures
    for name, fig in figures:
        if name == 'Net' and not self.boxes[self.NB].value:
            continue
        if name != 'Net' and not self.boxes[self.IT].value:
            continue
        if self.image_type.value == 'HTML':
            savehtml(fig, filename=loc/f'{name}.html')
        else:
            ext = self.image_type.value.lower()
            fn = loc/f'{name}.{ext}'
            fig.savefig(fn, bbox_inches='tight')
    if self.boxes[self.BS].value:
        df = self.op.summary.drop(['gates'], axis=1)
        df.to_csv(
            loc/'budget_summary.csv', 
            index=False,
            float_format=lambda n: round(n,2)
        )
    if self.boxes[self.BD].value:
        self.op.matrix.to_csv(
            loc/'barrier_details.csv',
            index=False,
            float_format=lambda n: round(n,2)
        )

InfoBox

Bases: Column

When the user clicks the Run Optimizer button in the Start panel the GUI displays a message by calling one of the methods in this class. Messages are displayed in the modal dialog area defined by the GUI template.

Parameters:
  • template

    the application template (which contains the modal dialog area to use)

  • run_cb

    a callback function to invoke after the user reviews settings and clicks "Continue"

Source code in src/gui/infobox.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def __init__(self, template, run_cb):
    """
    Initialize the module.

    Arguments:
      template:  the application template (which contains the modal dialog area to use)
      run_cb:  a callback function to invoke after the user reviews settings and clicks "Continue"
    """
    super(InfoBox, self).__init__()
    self.template = template

    self.continue_button = pn.widgets.Button(name='Continue')
    self.continue_button.on_click(run_cb)

    self.cancel_button = pn.widgets.Button(name='Cancel')
    self.cancel_button.on_click(self._cancel_cb)

_cancel_cb(_)

Close the dialog when the user clicks the "Cancel" button.

Source code in src/gui/infobox.py
60
61
62
63
64
def _cancel_cb(self, _):
    """
    Close the dialog when the user clicks the "Cancel" button.
    """
    self.template.close_modal()

show_missing(rlist, budget, tlist)

Method called by the OP class when it detects missing parameters (e.g. if the user did not select a region or a target).

Source code in src/gui/infobox.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def show_missing(self, rlist, budget, tlist):
    """
    Method called by the OP class when it detects missing parameters (e.g.
    if the user did not select a region or a target).
    """
    text = self.missing_params_text
    if len(rlist) == 0:
        text += ' * one or more geographic regions\n'
    if not budget:
        text += ' * a maximum budget\n'
    if len(tlist) == 0:
        text += ' * one or more targets\n'
    self.clear()
    self.append(pn.pane.Alert(text, alert_type = 'warning'))
    self.template.open_modal()

show_invalid_weights(w)

Method called when weighted targets are being used and one of the text boxes does not have a valid entry (must be a number between 1 and 5).

Parameters:
  • w (list[str]) –

    the list of strings read from the text entry widgets

Source code in src/gui/infobox.py
82
83
84
85
86
87
88
89
90
91
92
93
def show_invalid_weights(self, w: list[str]):
    """
    Method called when weighted targets are being used and one of the
    text boxes does not have a valid entry (must be a number between 1 and 5).

    Arguments:
      w: the list of strings read from the text entry widgets
    """
    text = self.invalid_weights_text.format(w)
    self.clear()
    self.append(pn.pane.Alert(text, alert_type = 'warning'))
    self.template.open_modal()

show_params(regions, budgets, targets, weights, mapping)

Method called to allow the user to review the optimization parameters read from the various widgets. Displays each parameter and two buttons ("Cancel" and "Continue").

Parameters:
  • regions

    list of region names

  • budgets

    a tuple with starting budget, increment, and count

  • targets

    list of restoration target names

  • weights

    list of target weights (optional)

  • mapping

    column mappings (optional)

Source code in src/gui/infobox.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def show_params(self, regions, budgets, targets, weights, mapping):
    """
    Method called to allow the user to review the optimization parameters read from the
    various widgets.  Displays each parameter and two buttons ("Cancel" and "Continue").

    Arguments:
      regions:  list of region names
      budgets:  a tuple with starting budget, increment, and count
      targets:  list of restoration target names
      weights:  list of target weights (optional)
      mapping:  column mappings (optional)
    """
    bstart, binc, bcount = budgets
    fbmax = OP.format_budget_amount(binc*bcount)
    fbstep = OP.format_budget_amount(binc)
    fbstart = OP.format_budget_amount(bstart)
    text = self.preview_message_text
    text += f'  * Regions: {", ".join(regions)}\n\n'
    if bcount > 1:
        text += f'  * {bcount} budget levels from {fbstep} up to {fbmax} in increments of {fbstep}\n\n'
    else:
        text += f'  * a single budget of {fbstart}\n\n'
    targets = [t.split(':')[-1] for t in targets]
    if weights:
        targets = [f'{targets[i]}{weights[i]}' for i in range(len(targets))]
    text += f'  * Targets: {", ".join(targets)}\n' 
    text += f'  * Mapping: {mapping}\n\n'
    self.clear()
    self.append(pn.pane.Alert(text, alert_type = 'secondary'))
    self.append(pn.Row(self.cancel_button, self.continue_button))
    self.template.open_modal()

show_success()

Method called after OptiPass has finished running and the results have been parsed successfully.

Source code in src/gui/infobox.py
127
128
129
130
131
132
133
134
def show_success(self):
    """
    Method called after OptiPass has finished running and the results have been
    parsed successfully.
    """
    self.clear()
    self.append(pn.pane.Alert(self.success_text, alert_type = 'success'))
    self.template.open_modal()

show_fail(reason)

Method called if OptiPass failed.

Parameters:
  • reason

    string containing the error message

Source code in src/gui/infobox.py
136
137
138
139
140
141
142
143
144
145
146
147
148
def show_fail(self, reason):
    """
    Method called if OptiPass failed.

    Arguments:
      reason:  string containing the error message
    """
    self.clear()
    text = self.fail_text.format(reason)
    if str(reason) == 'No solution':
        text += '\n * try increasing the maximum budget'
    self.append(pn.pane.Alert(text, alert_type = 'danger'))
    self.template.open_modal()

OutputPane

Bases: Column

After OptiPass has completed the last optimization run the GUI creates an instance of this class and saves it in the Output tab of the top level display.

(displayed in a tab widget showing one figure at a time), the second part has tables showing data about barriers included in solutions.

Parameters:
  • op

    an OPResult object with the optimization parameters and results

Source code in src/gui/output.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def __init__(self, op, tgmap):
    """
    Format the output from OptiPass.
    The first part of the panel has a set of ROI curves
    (displayed in a tab widget showing one figure at a time), the second
    part has tables showing data about barriers included in solutions.

    Arguments:
      op: an OPResult object with the optimization parameters and results
     """
    super(OutputPane, self).__init__()

    self.append(pn.pane.HTML('<h3>Optimization Results</h3>', styles=header_styles))
    self.append(self._make_title(op))

    if op.bcount > 1:
        self.append(pn.pane.HTML('<h3>ROI Curves</h3>'))
        self.append(self._make_figures_tab(op))

    self.append(pn.pane.HTML('<h3>Budget Summary</h3>'))
    self.gate_count = op.summary.gates.apply(len).sum()
    if self.gate_count == 0:
        self.append(pn.pane.HTML('<i>No barriers selected -- consider increasing the budget</i>'))
    else:
        self.append(self._make_budget_table(op, tgmap))
        self.append(pn.Accordion(
            ('Barrier Details', self._make_gate_table(op)),
            stylesheets = [accordion_style_sheet],
        ))

_make_title(op)

The top section of the output pane is a title showing the optimization parameters.

Source code in src/gui/output.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def _make_title(self, op):
    """
    The top section of the output pane is a title showing the optimization parameters.
    """
    tnames = [OP.target_frame.loc[t].short for t in op.targets]
    if op.weights:
        tnames = [f'{tnames[i]}{op.weights[i]}' for i in range(len(tnames))]

    title = f"<p><b>Regions:</b> {', '.join(op.regions)};"
    title += f" <b>Targets:</b> {', '.join(tnames)};"
    if s := OP.mapping_name:
        title += f" <b>{s.capitalize()}:</b> {op.mapping};"
    if op.bcount > 1:
        bmax = op.binc * op.bcount
        smin = OP.format_budget_amount(op.binc)
        smax = OP.format_budget_amount(bmax)
        title += f" <b>Budgets:</b> {smin} to {smax}</p>"
    else:
        b = OP.format_budget_amount(op.bmin),
        title += " <b>Budget:</b> {b}</p>"
    return pn.pane.HTML(title)

_make_figures_tab(op)

Create a Tabs object with one tab for each ROI curve.

Source code in src/gui/output.py
70
71
72
73
74
75
76
77
78
79
80
81
def _make_figures_tab(self, op):
    """
    Create a Tabs object with one tab for each ROI curve.
    """
    tabs = pn.Tabs(
        tabs_location='left',
        stylesheets = [output_tab_style_sheet],
    )
    op.make_roi_curves()
    for p in op.display_figures:
        tabs.append(p)
    return tabs

_make_budget_table(op, tgmap)

Make the table of benefits for each budget. Attach a callback function that is called when the user clicks on a row in the table (the callback updates the map to show gates used in a solution).

Source code in src/gui/output.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def _make_budget_table(self, op, tgmap):
    """
    Make the table of benefits for each budget.  Attach
    a callback function that is called when the user clicks on a row
    in the table (the callback updates the map to show gates used in a
    solution).
    """
    df = op.budget_table()
    formatters = { col: NumberFormatter(format='0.0', text_align='center') for col in df.columns }
    formatters['Budget'] = {'type': 'money', 'symbol': '$', 'precision': 0}
    formatters['# Barriers'] = NumberFormatter(format='0', text_align='center')
    alignment = { 
        'Budget': 'right',
        'Net Gain': 'center',
        '# Barriers': 'center'
    }

    table = pn.widgets.Tabulator(
        df,
        show_index = False,
        hidden_columns = ['gates'],
        editors = { c: None for c in df.columns },
        text_align = alignment,
        header_align = {c: 'center' for c in df.columns},
        formatters = formatters,
        selectable = True,
        configuration = {'columnDefaults': {'headerSort': False}},
    )
    self.budget_table = df
    self.make_dots(table, op, tgmap)
    table.on_click(self.budget_table_cb)

    return table

_make_gate_table(op)

Make a table showing details about gates used in solutions.

Source code in src/gui/output.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def _make_gate_table(self, op):
    """
    Make a table showing details about gates used in solutions.
    """
    formatters = { }
    alignment = { }
    df = op.gate_table()

    df.columns = [OP.format_budget_amount(int(s)) if s.isdigit() else s for s in df.columns]

    for col in df.columns:
        if col.startswith('$') or col in ['Primary','Dominant']:
            formatters[col] = {'type': 'tickCross', 'crossElement': ''}
            alignment[col] = 'center'
        elif col == 'Cost':
            formatters[col] = {'type': 'money', 'symbol': '$', 'precision': 0}
            alignment[col] = 'right'

    table = pn.widgets.Tabulator(
        df, 
        show_index=True, 
        frozen_columns=['ID'],
        hidden_columns=['count'],
        formatters=formatters,
        text_align=alignment,
        configuration={'columnDefaults': {'headerSort': False}},
        header_align={c: 'center' for c in df.columns},
        selectable = False,
    )
    table.disabled = True
    self.gate_table = df
    return table

make_dots(plot, op, tgmap)

Called after the output panel is initialized, make a set of glyphs to display for each budget level.

Source code in src/gui/output.py
150
151
152
153
154
155
156
157
158
159
160
161
def make_dots(self, plot, op, tgmap):
    """
    Called after the output panel is initialized, make a set of glyphs to display
    for each budget level.
    """
    self.selected_row = None
    self.dots = []
    for name, row in self.budget_table.iterrows():
        df = tgmap.map_coords().loc[row.gates]
        c = tgmap.map.circle_dot('x', 'y', size=12, line_color='blue', fill_color='white', source=df)
        c.visible = False
        self.dots.append(c)

budget_table_cb(e)

The callback function invoked when the user clicks a row in the budget table. Use the event to figure out which row was clicked. Hide any dots that were displayed previously, then make the dots for the selected row visible.

Source code in src/gui/output.py
163
164
165
166
167
168
169
170
171
172
def budget_table_cb(self, e):
    """
    The callback function invoked when the user clicks a row in the budget table.
    Use the event to figure out which row was clicked.  Hide any dots that were displayed
    previously, then make the dots for the selected row visible.
    """
    if n := self.selected_row:
        self.dots[n].visible = False
    self.selected_row = e.row
    self.dots[self.selected_row].visible = True

hide_dots()

Callback function invoked when users click on a region name in the start panel to hide any dots that might be on the map.

Source code in src/gui/output.py
174
175
176
177
178
179
180
181
def hide_dots(self):
    """
    Callback function invoked when users click on a region name in the start panel to hide
    any dots that might be on the map.
    """
    if self.selected_row:
        self.dots[self.selected_row].visible = False
    self.selected_row = None

RegionBox

Bases: Column

The region box displays the names of each geographic region in the data set, with a checkbox next to the name.

When the user clicks on one of the checkboxes several actions are triggered: the set of selected regions is updated, the budget widget is notified so it can update the maximum budget (based on the total cost of all barriers in the current selection), and the map is updated by zooming in to a level that contains only the barriers in the selected regions.

Parameters:
  • map

    the TGMap object that will be updated when regions are selected

  • budget

    the BudgetBox object to update when regions are selected

Source code in src/gui/regionbox.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def __init__(self, map, budget):
    """
    Create the grid of checkboxes and set up the callback function.

    Arguments:
      map:  the TGMap object that will be updated when regions are selected
      budget:  the BudgetBox object to update when regions are selected
    """
    super(RegionBox, self).__init__(margin=(10,0,10,5))
    self.map = map
    self.budget_box = budget
    self.boxes = { }
    for name in OP.region_names:
        box = pn.widgets.Checkbox(name=name, styles=box_styles, stylesheets=[box_style_sheet])
        box.param.watch(self.cb, ['value'])
        self.boxes[name] = box
    self.selected = set()
    self.external_cb = None
    self.append(pn.GridBox(*self.boxes.values(), ncols=3))

cb(*events)

Callback function invoked when one of the checkboxes is clicked. If the new state of the checkbox is 'selected' the region is added to the set of selected regions, otherwise it is removed. After updating the set notify the map widget and any other widgets that have been registered as external callbacks.

Source code in src/gui/regionbox.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def cb(self, *events):
    """
    Callback function invoked when one of the checkboxes is clicked.  If the new state
    of the checkbox is 'selected' the region is added to the set of selected regions,
    otherwise it is removed.  After updating the set notify the map widget and any
    other widgets that have been registered as external callbacks.
    """
    for e in events:
        if e.type == 'changed':
            r = e.obj.name
            if e.new:
                self.selected.add(r)
            else:
                self.selected.remove(r)
            amount = sum(OP.total_cost[x] for x in self.selected)
            self.budget_box.set_budget_max(amount)
    self.map.display_regions(self.selected)
    # self.map.zoom(self.selected)
    if self.external_cb:
        self.external_cb()

check(region)

Initialize the box by addidng region to the selection.

Source code in src/gui/regionbox.py
62
63
64
65
66
def check(self, region):
    """
    Initialize the box by addidng region to the selection.
    """
    self.boxes[region].value = True

selection()

Return a list of the names of currently selected regions.

Source code in src/gui/regionbox.py
68
69
70
71
72
def selection(self) -> list[str]:
    """
    Return a list of the names of currently selected regions.
    """
    return list(self.selected)

add_external_callback(f)

Save a reference to an external function to call when a region box is clicked.

Parameters:
  • f

    aditional function to call when a checkbox is clicked

Source code in src/gui/regionbox.py
74
75
76
77
78
79
80
81
def add_external_callback(self, f):
    """
    Save a reference to an external function to call when a region box is clicked.

    Arguments:
      f: aditional function to call when a checkbox is clicked
    """
    self.external_cb = f

TargetBox

Bases: Column

The restoration targets are shown in a matrix with a selection widget next to each target name. The TargetBox widget has two tabs showing different types of selection widgets, either simple checkboxes (shown by a BasicTargetBox) or text entry widgets (shown by WeightedTargetBox).

Source code in src/gui/targetbox.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def __init__(self):
    super(TargetBox, self).__init__(margin=(10,0,10,5))

    if OP.mapping_name:
        lst = [s.capitalize() for s in OP.target_columns]
        label = OP.mapping_name.capitalize()
        self.mapping_buttons = pn.widgets.RadioBoxGroup(name=label, options=lst)
        mapping_box = pn.Column(
            pn.pane.HTML(f'<b>{label}<b>', align='start'),
            self.mapping_buttons,
        )
    else:
        self.mapping_buttons = None

    self.tabs = pn.Tabs(
        ('Basic', BasicTargetBox()),
        ('Weighted', WeightedTargetBox()),
    )
    row = pn.Row()
    row.append(self.tabs)
    if self.mapping_buttons:
        row.append(mapping_box)
    self.append(row)

make_layout(obj) staticmethod

Read the target layout (size of grid, location of each target in the grid)

Source code in src/gui/targetbox.py
40
41
42
43
44
45
46
47
@staticmethod
def make_layout(obj):
    """
    Read the target layout (size of grid, location of each target in the grid)
    """
    obj.layout = [s.split() for s in OP.target_layout]
    obj.nrows = len(obj.layout)
    obj.ncols = max(len(r) for r in obj.layout)

selection()

Get a list of IDs of selected targets from the current target widget.

Source code in src/gui/targetbox.py
49
50
51
52
53
def selection(self) -> list[str]:
    """
    Get a list of IDs of selected targets from the current target widget.
    """
    return self.tabs[self.tabs.active].selection()

weights()

Get target weights from the current target widget.

Source code in src/gui/targetbox.py
55
56
57
58
59
def weights(self):
    """
    Get target weights from the current target widget.
    """
    return self.tabs[self.tabs.active].weights()

mapping()

If the targets have alternative column name mappings return the selected mapping name

Source code in src/gui/targetbox.py
61
62
63
64
65
66
def mapping(self):
    """
    If the targets have alternative column name mappings return the selected
    mapping name
    """
    return self.mapping_buttons.value.lower() if OP.mapping_name else None

BasicTargetBox

Bases: Column

The BasicTargetBox widget displays a checkbox next to each target name.

Source code in src/gui/targetbox.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def __init__(self):
    """
    Make the grid of checkboxes.  The IDs and descriptions of targets are
    fetched by calling the make_layout function in the Target class.
    """
    super(BasicTargetBox, self).__init__(margin=(10,0,10,5))
    TargetBox.make_layout(self)
    self.grid = pn.GridBox(nrows = self.nrows, ncols = self.ncols)
    for row in self.layout:
        for t in row:
            s = OP.target_frame.loc[t].long
            b = pn.widgets.Checkbox(name=s, styles=box_styles, stylesheets=[box_style_sheet], tags=[t])
            self.grid.append(b)
    self.append(self.grid)

selection()

Return a list of IDs of selected targets.

Source code in src/gui/targetbox.py
91
92
93
94
95
def selection(self) -> list[str]:
    """
    Return a list of IDs of selected targets.
    """
    return [b.tags[0] for b in self.grid.objects if b.value]

weights()

There are no weights (all targets considered equally) so return an empty list.

Source code in src/gui/targetbox.py
102
103
104
105
106
def weights(self):
    """
    There are no weights (all targets considered equally) so return an empty list.
    """
    return []

WeightedTargetBox

Bases: Column

A WeightedTargetBox shows a text entry widget next to each target to allow users to enter a numeric weight for the target.

Source code in src/gui/targetbox.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def __init__(self):
    """
    Make the grid of text entry widgets.  The IDs and descriptions of targets are
    fetched by calling the make_layout function in the Target class.
    """
    super(WeightedTargetBox, self).__init__(margin=(10,0,10,5))
    TargetBox.make_layout(self)
    self.grid = pn.GridBox(nrows = self.nrows, ncols = self.ncols)
    for row in self.layout:
        for t in row:
            s = OP.target_frame.loc[t].long
            w = pn.Row()
            w.append(pn.widgets.TextInput(name='', placeholder='', width=25, align='center', stylesheets=[input_style_sheet], tags=[t]))
            w.append(pn.pane.HTML(s))
            self.grid.append(w)
    self.append(self.grid)

selection()

Return a list of IDs of selected targets.

Source code in src/gui/targetbox.py
131
132
133
134
135
def selection(self) -> list[str]:
    """
    Return a list of IDs of selected targets.
    """
    return [w[0].tags[0] for w in self.grid.objects if w[0].value]

weights()

Return the text content of each non-empty text entry box.

Source code in src/gui/targetbox.py
142
143
144
145
146
def weights(self) -> list[str]:
    """
    Return the text content of each non-empty text entry box.
    """
    return [w[0].value for w in self.grid.objects if w[0].value]

TGMap

A TGMap object manages the display of a map that shows the locations of the barriers in a project.

A static method named init is a factory that instantiates a new map. It will read the "mapinfo" file for the project and return a reference to a new map object belonging to the class specified in the mapinfo file.

Attributes:
  • map

    a Bokeh figure object, with x and y ranges defined by the locations of the barriers

  • dots

    a dictionary that maps region names to a list of circle glyphs for each barrier in a region

  • ranges

    a data frame that has the range of x and y coordinates for each region

map_coords()

Return a frame that has the coordinates and other info needed to display gates on a map

Source code in src/gui/tgmap.py
40
41
42
43
44
45
def map_coords(self):
    '''
    Return a frame that has the coordinates and other info needed to display
    gates on a map
    '''
    return self._map_coords

graphic()

Return a reference to the map (a Bokeh figure).

Source code in src/gui/tgmap.py
47
48
49
50
51
def graphic(self):
    '''
    Return a reference to the map (a Bokeh figure).
    '''
    return self.map

display_regions(selection)

Method called when the user clicks the checkbox next to the name of a region. Set the visible attribute of each dot to True or False depending on whether the region it is in is selected.

Parameters:
  • selection

    a list of names of regions currently selected

Source code in src/gui/tgmap.py
53
54
55
56
57
58
59
60
61
62
63
def display_regions(self, selection):
    """
    Method called when the user clicks the checkbox next to the name
    of a region.  Set the visible attribute of each dot to True or False depending
    on whether the region it is in is selected.

    Arguments:
      selection:  a list of names of regions currently selected
    """
    for r, dots in self.dots.items():
        dots.visible = r in selection

StaticMap

Bases: TGMap

A static map is simply a PNG file downloaded from the server.

Source code in src/gui/tgmap.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def __init__(self):
    url = f"{OP.server_url}/map/{OP.project_name}/{OP.mapinfo['map_file']}"
    logging.info(f'Fetching map from {url}')
    xpixels = 473
    ypixels = 533
    p = bk.figure(
        title=OP.mapinfo['map_title'],
        x_range=(0,xpixels), 
        y_range=(0,ypixels),
        tools=OP.mapinfo['map_tools'],
        tooltips = [
            ("Barrier", "@ID"),
            ("Region", "@region"),
            ("Cost", "@cost")
        ]
    )
    p.image_url(url=[url], x = 0, y = ypixels, h = ypixels, w = xpixels)
    bf = OP.barrier_frame
    self.dots = { }
    for r in OP.region_names:
        df = bf[bf.region == r]
        c = p.circle('X','Y', size=10, color='darkslategray', source=df)
        self.dots[r] = c
        c.visible = False
    self.map = p

    df = bf[['region','X','Y']]
    df.columns = ['region','x','y']
    self._map_coords = df

TiledMap

Bases: TGMap

A tiled map uses a tile server to fetch the map image. Fetch the main barrier file to get the coordinates and other data for each barrier.

Source code in src/gui/tgmap.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def __init__(self):
    # bf = self._fetch_barriers()
    self._map_coords = self._make_info(OP.barrier_frame)
    self.regions = self._make_region_list()
    self.ranges = self._create_ranges()
    self.tile_provider = get_provider(xyz.OpenStreetMap.Mapnik)
    p = bk.figure(
        title='Oregon Coast', 
        height=900,
        width=400,
        x_range=(self._map_coords.x.min()*0.997,self._map_coords.x.max()*1.003), 
        y_range=(self._map_coords.y.min()*0.997,self._map_coords.y.max()*1.003),
        x_axis_type='mercator',
        y_axis_type='mercator',
        toolbar_location='below',
        tools=['pan','wheel_zoom','hover','reset'],
        tooltips = [
            ("ID", "@ID"),
            ("Region", "@region"),
            ("Type", "@type"),
        ]
    )
    p.add_tile(self.tile_provider)
    p.toolbar.autohide = True
    self.dots = { }
    for r in self.regions:
        df = self._map_coords[self._map_coords.region == r]
        c = p.circle('x', 'y', size=5, color='darkslategray', source=df, tags=list(df.index))
        self.dots[r] = c
        c.visible = False

    self.map = p

    self.outer_x = (self._map_coords.x.min()*0.997,self._map_coords.x.max()*1.003)
    self.outer_y = (self._map_coords.y.min()*0.997,self._map_coords.y.max()*1.003)

_make_info(bf)

Hidden method, makes a dataframe with attributes needed to display gates on a map. Map latitude and longitude columns in the input frame to Mercator coordinates, and copy the ID, region and barrier types so they can be displayed as tooltips.

Source code in src/gui/tgmap.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def _make_info(self, bf):
    """
    Hidden method, makes a dataframe with attributes needed to display gates on a map.
    Map latitude and longitude columns in the input frame to Mercator
    coordinates, and copy the ID, region and barrier types so they can
    be displayed as tooltips.
    """
    df = bf[['region','type']]
    R = 6378137.0
    map_coords = pd.concat([
        df, 
        np.radians(bf.X)*R, 
        np.log(np.tan(np.pi/4 + np.radians(bf.Y)/2)) * R
    ], axis=1)
    map_coords.columns = ['region', 'type', 'x', 'y']
    return map_coords

_make_region_list()

Hidden method, make a list of unique region names, sorted by latitude, so regions are displayed in order from north to south. Updates the list of names in the OP object.

Source code in src/gui/tgmap.py
171
172
173
174
175
176
177
178
179
180
181
def _make_region_list(self):
    '''
    Hidden method, make a list of unique region names, sorted by latitude, so regions
    are displayed in order from north to south.  Updates the list of names in the
    OP object.
    '''
    df = self._map_coords[['region','y']]
    mf = df.groupby('region').mean(numeric_only=True).sort_values(by='y',ascending=False)
    names = list(mf.index)
    OP.region_names = names
    return names

_create_ranges()

Hidden method, called by the constructor to create a Pandas Dataframe containing the range of latitudes and longitudes of the barriers in a project.

Source code in src/gui/tgmap.py
183
184
185
186
187
188
189
190
191
192
193
194
195
def _create_ranges(self):
    """
    Hidden method, called by the constructor to create a Pandas Dataframe 
    containing the range of latitudes and longitudes of the barriers in
    a project.
    """
    g = self._map_coords.groupby('region')
    return pd.DataFrame({
        'x_min': g.min().x,
        'x_max': g.max().x,
        'y_min': g.min().y,
        'y_max': g.max().y,
    })

display_regions(selection)

Update the map, setting the x and y range based on the currently selected regions.

Parameters:
  • selection

    a list of names of regions currently selected

Source code in src/gui/tgmap.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def display_regions(self, selection):
    """
    Update the map, setting the x and y range based on the currently selected
    regions.

    Arguments:
      selection:  a list of names of regions currently selected
    """
    super().display_regions(selection)
    if len(selection) > 0:
        xmin = min([self.ranges['x_min'][r] for r in selection])
        xmax = max([self.ranges['x_max'][r] for r in selection])
        ymin = min([self.ranges['y_min'][r] for r in selection])
        ymax = max([self.ranges['y_max'][r] for r in selection])

        mx = (xmax+xmin)/2
        my = (ymax+ymin)/2
        dx = max(5000, xmax - xmin)
        dy = max(5000, ymax - ymin)
        ar = self.map.height / self.map.width

        if dy / dx > ar:
            dx = dy / ar
        else:
            dy = dx * ar

        self.map.x_range.update(start=mx-dx/2-5000, end=mx+dx/2+5000)
        self.map.y_range.update(start=my-dy/2, end=my+dy/2)
    else:
        self.map.x_range.update(start=self.outer_x[0], end=self.outer_x[1])
        self.map.y_range.update(start=self.outer_y[0], end=self.outer_y[1])
    self.map.add_tile(self.tile_provider)