WAP-Tool 0.1.0__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.
wap/__init__.py ADDED
File without changes
Binary file
wap/static/base.css ADDED
@@ -0,0 +1,305 @@
1
+ /*
2
+ * style.css
3
+ * Copyright © 2026 Fred W6BSD
4
+ *
5
+ * Distributed under terms of the BSD 3-Clause license.
6
+ */
7
+ body {
8
+ background-color: #fdfdff;
9
+ color: #333;
10
+ display: flex;
11
+ flex-direction: column;
12
+ font-family: -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
13
+ font-size: 16px;
14
+ line-height: 1.6em;
15
+ margin: 0;
16
+ min-height: 100vh;
17
+ padding: 0;
18
+ }
19
+
20
+ header {
21
+ background: linear-gradient(135deg, #0a2342, #1c3d73);
22
+ color: #DAF7A6;
23
+ font-family: Arial, sans-serif;
24
+ padding: 1rem 0;
25
+ text-align: center;
26
+ }
27
+
28
+ header h1 {
29
+ margin: 1em 0;
30
+ text-align: center;
31
+ font-size: 3rem;
32
+ }
33
+
34
+ header ul {
35
+ list-style: none;
36
+ margin: 0;
37
+ padding: 0;
38
+
39
+ display: flex;
40
+ justify-content: center; /* center horizontally */
41
+ gap: 1.5rem; /* spacing between items */
42
+ }
43
+
44
+ header li a {
45
+ color: #fff;
46
+ background: #335;
47
+ text-decoration: none;
48
+ padding: 0.5rem 1rem;
49
+ border-radius: 4px;
50
+ border: 1px solid #777;
51
+
52
+ }
53
+
54
+ header ul a:hover,
55
+ header ul a:focus {
56
+ background: #337;
57
+ }
58
+
59
+ header ul a[aria-current="page"] {
60
+ background: #686;
61
+ }
62
+
63
+ main {
64
+ flex: 1;
65
+ padding: 3em;
66
+ padding-bottom: 5em;
67
+ border-left: 2px solid #ccc;
68
+ border-right: 2px solid #ccc;
69
+ margin: 0 auto;
70
+ width: 900px;
71
+ }
72
+
73
+ main h2 {
74
+ border-top: 4px solid #f80;
75
+ color: #512E5F;
76
+ font-size: 2em;
77
+ padding-right: 1.7em;
78
+ padding-top: .25em;
79
+ text-align: left;
80
+ }
81
+
82
+ main h3 {
83
+ color: #512E5F;
84
+ background-color: #ddeeff;
85
+ padding: 1em 0 .5em .5em;
86
+ border-bottom: 3px solid #f80;
87
+ font-family: Helvetica, sans-serif;
88
+ font-weight: Bold;
89
+ margin-top: 2em;
90
+ }
91
+
92
+ main .container {
93
+ margin-left: 2.5em;
94
+ margin-top: 2.5em;
95
+ }
96
+
97
+ main a {
98
+ text-decoration: none;
99
+ font-weight: bold;
100
+ }
101
+
102
+ main a:hover {
103
+ text-decoration: underline;
104
+ }
105
+
106
+ main img {
107
+ border-radius: 6px;
108
+ width: 100%;
109
+ }
110
+
111
+ table.power {
112
+ width: 50%;
113
+ font-family: Arial;
114
+ border-collapse: collapse;
115
+ margin: 20px auto;
116
+ background: white;
117
+ border-radius: 5px;
118
+ overflow: hidden;
119
+ box-shadow: 0 1px 3px rgba(0,0,0,0.5);
120
+ }
121
+
122
+ .power th {
123
+ background-color: #4a90e2;
124
+ color: white;
125
+ padding: 6px;
126
+ text-align: center;
127
+ font-weight: 600;
128
+ border: 1px solid #777;
129
+ }
130
+
131
+ .power td {
132
+ padding: 6px 12px;
133
+ border-bottom: 1px solid #eee;
134
+ text-align: right;
135
+ border: 1px solid #ddd;
136
+ }
137
+
138
+ .power tr:nth-child(even) {
139
+ background-color: #f8f9fa;
140
+ }
141
+
142
+ .power tr:hover {
143
+ background-color: #e3f2fd;
144
+ }
145
+
146
+ .power tr:last-child td {
147
+ border-bottom: none;
148
+ }
149
+
150
+ .form-container {
151
+ background-color: white;
152
+ padding: 20px;
153
+ border-radius: 8px;
154
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
155
+ width: 100%;
156
+ max-width: 600px;
157
+ margin: 2em auto;
158
+ }
159
+
160
+ .form-container h2 {
161
+ all: unset;
162
+ font-family: 'Arial', sans-serif;
163
+ font-size: 24px;
164
+ font-weight: 600;
165
+ color: #0a2342;
166
+ text-align: center;
167
+ display: block;
168
+ }
169
+
170
+ .form-container hr {
171
+ border: 0;
172
+ border-top: 1px solid #0a2342;
173
+ margin: 1.5rem 0;
174
+ }
175
+
176
+ .form-group {
177
+ margin-bottom: 20px;
178
+ }
179
+
180
+ .form-group label {
181
+ font-weight: bold;
182
+ display: block;
183
+ margin-bottom: 8px;
184
+ color: #555;
185
+ font-weight: 600;
186
+ }
187
+
188
+ .form-group input,
189
+ .form-group textarea {
190
+ width: 100%;
191
+ padding: 12px;
192
+ border: 1px solid #ddd;
193
+ border-radius: 6px;
194
+ font-size: 16px;
195
+ box-sizing: border-box;
196
+ }
197
+
198
+ .form-group textarea {
199
+ height: 120px;
200
+ resize: vertical;
201
+ }
202
+
203
+ button[type="submit"] {
204
+ background-color: #4CAF50;
205
+ color: white;
206
+ border: none;
207
+ padding: 12px 20px;
208
+ border-radius: 6px;
209
+ cursor: pointer;
210
+ font-size: 16px;
211
+ width: 100%;
212
+ transition: background-color 0.3s;
213
+ }
214
+
215
+ button[type="submit"]:hover {
216
+ background-color: #45a049;
217
+ }
218
+
219
+ select {
220
+ padding: 10px 15px;
221
+ border: 1px solid #ddd;
222
+ border-radius: 8px;
223
+ background-color: white;
224
+ font-size: 16px;
225
+ color: #333;
226
+ width: 200px;
227
+ height: 45px;
228
+ appearance: none;
229
+ -webkit-appearance: none;
230
+ -moz-appearance: none;
231
+ cursor: pointer;
232
+ }
233
+
234
+ footer {
235
+ font-size: .8em;
236
+ background-color: #322;
237
+ text-align: right;
238
+ color: #cccccc;
239
+ line-height: 1.1rem;
240
+ margin-top: 0em;
241
+ padding: 1em 4em;
242
+ }
243
+
244
+ footer a {
245
+ color: yellow;
246
+ text-decoration: none;
247
+ }
248
+
249
+ footer a:hover {
250
+ text-decoration: underline;
251
+ }
252
+
253
+ .result {
254
+ background: #f8fafc;
255
+ border: 1px solid #e2e8f0;
256
+ border-radius: 6px;
257
+ padding: 16px;
258
+ margin: 20px 0;
259
+ }
260
+
261
+ .error,
262
+ .status {
263
+ border-radius: 8px;
264
+ padding: 1.5em 2em;
265
+ margin: 2em 3em;
266
+ }
267
+
268
+ .status {
269
+ background-color: #dfd;
270
+ border: 3.5px solid #daa;
271
+ }
272
+
273
+ .error {
274
+ color: #fff;
275
+ font-weight: bold;
276
+ background-color: #f44;
277
+ border: 3.5px solid #faa;
278
+ }
279
+
280
+ .center {
281
+ text-align: center;
282
+ }
283
+
284
+ .band-pill-group input[type=checkbox] { display: none; }
285
+
286
+ .band-pill-group label {
287
+ cursor: pointer;
288
+ padding: 2px 10px;
289
+ border-radius: 999px;
290
+ border: 1px solid #ccc;
291
+ font-size: 12px;
292
+ color: #333;
293
+ background-color: #efe;
294
+ user-select: none;
295
+ display: inline-block;
296
+ font-weight: 500;
297
+ margin-bottom: 0;
298
+ }
299
+
300
+ .band-pill-group input[type=checkbox]:checked + label {
301
+ background: #dbeaff;
302
+ color: #55d;
303
+ border-color: #55d;
304
+ font-weight: 500;
305
+ }
wap/static/perf.css ADDED
@@ -0,0 +1,73 @@
1
+ /*
2
+ * perf.css
3
+ * Copyright © 2026 Fred W6BSD
4
+ *
5
+ * Distributed under terms of the BSD 3-Clause license.
6
+ */
7
+
8
+ .legend {
9
+ line-height: 1.6em;
10
+ padding-left: 2em;
11
+ padding-right: 2em;
12
+ margin-bottom: 1.5em;
13
+ }
14
+
15
+ .note {
16
+ background-color: #eeffee;
17
+ border-radius: 8px;
18
+ padding: 1.5em 2em;
19
+ margin: 2em 3em;
20
+ border: 3.5px solid #ada;
21
+ }
22
+
23
+ #stats {
24
+ font-family: "Avenir Next", "Avenir", Arial, Helvetica, Roboto, sans-serif;
25
+ margin: 3rem 6rem;
26
+ border: 1px solid #aaa;
27
+ border-radius: 8px;
28
+ background-color: #ddd;
29
+ }
30
+
31
+ #stats .title {
32
+ font-weight: bold;
33
+ padding: .5em;
34
+ border-bottom: 1px solid #aba;
35
+ }
36
+
37
+ #stats ul {
38
+ border-radius: 0 0 8px 8px;
39
+ background-color: #eee;
40
+ list-style: none;
41
+ padding: 1.2rem;
42
+ margin: 0;
43
+ }
44
+
45
+ #stats li {
46
+ padding: 2px 0;
47
+ color: #333;
48
+ }
49
+
50
+ table.results {
51
+ margin-left: 2em;
52
+ border-collapse: collapse;
53
+ }
54
+
55
+ .results td, .results th {
56
+ padding: 5px 12px;
57
+ border: 1px solid #ccc;
58
+ }
59
+
60
+ .warning {
61
+ margin-left: 1em;
62
+ padding: 0 1em;
63
+ font-weight: bold;
64
+ background-color: red;
65
+ color: white;
66
+ }
67
+ .dim {
68
+ color: #f99;
69
+ }
70
+ .highlight {
71
+ color: #66c;
72
+ font-weight: 600;
73
+ }
@@ -0,0 +1,296 @@
1
+ {% extends "perfbase.html" %}
2
+
3
+ {% block title %}{{ title }}{% endblock %}
4
+
5
+ {% block content %}
6
+
7
+ <div class="note">
8
+ Because the plots are built from real reception reports rather than models, they provides a
9
+ <b>practical view of how the antenna performs</b> on the air, helping to compare bands, antenna
10
+ configurations, and installation changes under real propagation conditions.
11
+ </div>
12
+
13
+ <div id="stats">
14
+ <div class="title">Antenna performance calculated by <a href="https://wspr.bsdworld.org/">wspr.bsdword.org</a> using <a href="https://www.wsprnet.org/drupal/wsprnet/map">WSPR data</a>.</div>
15
+ <ul>
16
+ <li>Report date: <b>{{ date.strftime('%Y-%m-%d %H:%M') }} UTC</b></li>
17
+ <li>Station call sign: <b>{{ call }}</b></li>
18
+ <li>Grid locator: <b>{{ data.iloc[0]['tx_loc'] }}</b></li>
19
+ {% if antenna %}
20
+ <li>Antenna: <b>{{ antenna }}</b></li>
21
+ {% endif %}
22
+ <li>WSPR data:
23
+ <b>from: {{ start.strftime('%Y-%m-%d %H:%M') }}
24
+ <span style="display: inline-block; width: 10px;"></span>
25
+ to: {{ end.strftime('%Y-%m-%d %H:%M') }}</b>
26
+ </li>
27
+ <li>
28
+ {% if data.shape[0] < 150 %}
29
+ Data processed: <b>{{ data.shape[0] }}</b> <span class="warning">You don't have enough spots</span>
30
+ {% else %}
31
+ Data processed: <b>{{ data.shape[0] }}</b>
32
+ {% endif %}
33
+ </li>
34
+ <li>Transmit power anounced:
35
+ {% if data.power.min() == data.power.max() %}
36
+ <b>{{ data.power.mean() | int }}</b> dBm <i>({{ "{:.3f}".format(10**((data.power.mean() - 30) / 10)) }}Watt)</i>
37
+ {% else %}
38
+ Min: <b>{{ data.power.min() | int }}</b> dBm -
39
+ Avg: <b>{{ data.power.mean() | int }}</b> dBm <i>({{ "{:.3f}".format(10**((data.power.mean() - 30) / 10)) }}W)</i> -
40
+ Max: <b>{{ data.power.max() | int }}</b> dBm
41
+ {% endif %}
42
+ </li>
43
+ <li>Max distance: <b>{{ data.distance.max() }}</b> km</li>
44
+ <li>Band tested:
45
+ {% if data.band.unique() | length > 1 %}
46
+ <table class="results">
47
+ <tr><th>Band</th><th>Spots</th><th>Comment</th></tr>
48
+ {% set errors = [] %}
49
+ {% for b in data.band.unique() | sort %}
50
+ {% set count = data[data.band == b]['id'].count() %}
51
+ <tr>
52
+ <td style="text-align: right;">{{ b }}</td>
53
+ <td style="text-align: left;">{{ count }}</td>
54
+ <td style="text-align: left;">
55
+ {% if count < 100 %}
56
+ <span class="dim">Not enough spots for an accurate assessment</span>
57
+ {% else %}
58
+ <span class="highlight">OK</span>
59
+ {% endif %}
60
+ </td>
61
+ </tr>
62
+ {% if count < 100 %}{{ errors.append(1) or "" }}{% endif %}
63
+ {% endfor %}
64
+ </table>
65
+ {% else %}
66
+ {% for b in data.band.unique() | sort %}
67
+ <b>{{ b }}</b>{% if not loop.last %}, {% endif %}
68
+ {% endfor %}
69
+ {% endif %}
70
+ </li>
71
+ </ul>
72
+ </div>
73
+
74
+ {% if errors %}
75
+ <div class="warning">
76
+ Remove bands with insufficient spots before generating the report.
77
+ </div>
78
+ {% endif %}
79
+
80
+ {% if "azimuth_scatter" in graphs %}
81
+ <div class="container">
82
+ <h2 id="azimuth_scatter">Distance and density</h2>
83
+ <div class="legend">
84
+ <p>This polar scatter plot shows the <b>75th percentile</b> of WSPR spot distances by azimuth from the transmitting station. Each point marks the distance below which 75% of WSPR reports were received in that direction. The angle shows the receiving station's direction, and the radius shows this distance.</p>
85
+
86
+ <p>Clusters of points in certain directions show where the antenna system and propagation support stronger and more reliable signals. On the other hand, gaps or shorter distances might mean there are directional nulls, local obstacles, ground effects near the antenna, <b>or not many receiving stations</b> in those directions.</p>
87
+ </div>
88
+ <img src="{{ graphs["azimuth_scatter"][1] }}" alt="{{ graphs["azimuth_scatter"][0] }}">
89
+ </div>
90
+ {% endif %}
91
+
92
+ {% if "radiation_pattern" in graphs %}
93
+ <div class="container">
94
+ <h2 id="radiation_pattern">Radiation pattern</h2>
95
+ <div class="legend">
96
+ <p>This polar plot shows the <b>75th percentile</b> of WSPR reception distance by azimuth, which is the range where contacts are almost always possible. In each direction, the line marks how far a reliable signal can reach. Lobes in some directions show where the antenna and propagation support stronger, more reliable signals. Nulls indicate obstacles, ground effects, or just fewer receiving stations in those directions. This approach makes it easy to compare how well the antenna works in different directions and on different bands.</p>
97
+
98
+ <p>When the plot shows multiple bands, the differences in shape and size reveal how the antenna and propagation change with frequency. This makes it easier to compare how well the antenna works in different directions and how far it can reach on each band during real operating conditions.</p>
99
+ </div>
100
+ <img src="{{ graphs["radiation_pattern"][1] }}" alt="{{ graphs["radiation_pattern"][0] }}">
101
+ </div>
102
+ {% endif %}
103
+
104
+ {% if "band_distance" in graphs %}
105
+ <div class="container">
106
+ <h2 id="band_distance">Distance / band</h2>
107
+ <div class="legend">
108
+ <p>This <a href="javascript:void(0);" onclick="show_help('boxplot');" style="color: #0066cc; cursor: help; text-decoration: underline dashed;">boxplot</a> summarizes the distribution of WSPR reception distances for each band.</p>
109
+
110
+ <p>The median and quartiles indicate the typical propagation range and its variability, while the whiskers and outliers represent less common short- and long-distance paths. Comparing bands shows how propagation and antenna effectiveness vary with frequency, offering a concise view of which bands provide consistent coverage or occasional long-distance reach.</p>
111
+ <p><b>Extreme outliers have been removed</b> to more accurately reflect antenna performance.</p>
112
+ </div>
113
+ <img src="{{ graphs["band_distance"][1] }}" alt="{{ graphs["band_distance"][0] }}">
114
+ </div>
115
+ {% endif %}
116
+
117
+ {% if "hours_distance" in graphs %}
118
+ <div class="container">
119
+ <h2 id="hours_distance">Distance / hour</h2>
120
+ <div class="legend">
121
+ <p>This <a href="javascript:void(0);" onclick="show_help('boxplot');" style="color: #0066cc; cursor: help; text-decoration: underline dashed;">boxplot</a> shows the distribution of WSPR reception distances across all bands by hour of day.</p>
122
+
123
+ <p>The median and spread show how the propagation range changes throughout the day, with longer distances at night and shorter distances during the day. Aggregating multiple spots allows the plot to highlight when each band-antenna combination is most effective for reaching distant receivers. Extreme outliers have been removed to improve the accuracy of antenna performance assessment.</p>
124
+ </div>
125
+ <img src="{{ graphs["hours_distance"][1] }}" alt="{{ graphs["hours_distance"][0] }}">
126
+ </div>
127
+ {% endif %}
128
+
129
+ {% if "plot_snr_vs_distance" in graphs %}
130
+ <div class="container">
131
+ <h2 id="plot_snr_vs_distance">Signal strength vs distance</h2>
132
+ <div class="legend">
133
+ <p>This graph shows how signal strength (SNR) changes with distance.</p>
134
+
135
+ <p>The median SNR shows the typical signal strength you can expect at each distance, and it drops as distance increases. The 10th and 90th percentile curves show the usual range of performance. As the gap between these percentiles widens at longer distances, it means there is more variation in long-path propagation.</p>
136
+ </div>
137
+ <img src="{{ graphs["plot_snr_vs_distance"][1] }}" alt="{{ graphs["plot_snr_vs_distance"][0] }}">
138
+ </div>
139
+ {% endif %}
140
+
141
+ {% if "snr_distance_errorbar" in graphs %}
142
+ <div class="container">
143
+ <h2 id="snr_distance">SNR vs distance</h2>
144
+ <div class="legend">
145
+ <p>The vertical bars show the typical signal strength (SNR) at each distance. The light blue line shows how many spots occur at each distance, as shown on the right-side axis.</p>
146
+
147
+ <p>The graph below shows how signal strength changes with distance. Signal-to-noise ratio (SNR) measures how clear the signal is compared to background noise, and it usually drops as you move farther away. A DX antenna can help keep the signal stronger over longer distances.</p>
148
+ </div>
149
+ <img src="{{ graphs["snr_distance_errorbar"][1] }}" alt="{{ graphs["snr_distance_errorbar"][0] }}">
150
+ </div>
151
+ {% endif %}
152
+
153
+ {% if "takeoff_angle" in graphs %}
154
+ <div class="container">
155
+ <h2 id="takeoff_angle">Estimated takeoff angle</h2>
156
+ <div class="legend">
157
+ <p>The chart shows the estimated antenna <a href="javascript:void(0);" onclick="show_help('takeoff');" style="color: #0066cc; cursor: help; text-decoration: underline dashed;">takeoff angles</a> ranges for each frequency band, based on contact distances. Each arc represents a band and highlights the range of angles linked to the most successful contacts.</p>
158
+
159
+ <p>Higher takeoff angles lead to shorter, more local contacts, while lower angles are linked to longer-distance (DX) paths. Bands with arcs closer to 0° have more low-angle radiation, which is usually better for long-distance communication.</p>
160
+ </div>
161
+ <img src="{{ graphs["takeoff_angle"][1] }}" alt="{{ graphs["takeoff_angle"][0] }}">
162
+ </div>
163
+ {% endif %}
164
+
165
+ {% if map %}
166
+ <div class="container">
167
+ <h2 id="graph-7}">Spot map</h2>
168
+ <div class="legend">
169
+ <p>This interactive map shows all the information for each spot.</p>
170
+ </div>
171
+ {{ map|safe }}
172
+ </div>
173
+ {% endif %}
174
+
175
+ <!-- popup help -->
176
+ <div id="boxplot" class="modal" style="display: none;">
177
+ <div class="modal-card">
178
+ <span class="close">&times;</span>
179
+ <div class="help_title">Understanding Boxplots</div>
180
+ <div class="modal-content">
181
+ <p>A boxplot is a visual tool that displays how data is distributed. It shows the middle 50% of the data in a box, with the median marked by a line inside.</p>
182
+ <p>The ends of the box represent the 25th and 75th percentiles, while whiskers extend to the smallest and largest values. Outliers appear as individual points beyond the whiskers.</p>
183
+ <p>Boxplot on <a href="https://en.wikipedia.org/wiki/Box_plot" target="wikipedia">Wikipedia</a>.
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <div id="takeoff" class="modal" style="display: none;">
189
+ <div class="modal-card">
190
+ <span class="close">&times;</span>
191
+ <div class="help_title">Understanding Takeoff Angle</div>
192
+ <div class="modal-content">
193
+ <p><b>Median takeoff angle decreases with increasing band frequency.</b></p>
194
+ <ul>
195
+ <li>Higher angles (40-70°) optimal for regional/NVIS propagation</li>
196
+ <li>Moderate angles (10-45°) supporting mixed propagation modes</li>
197
+ <li>Lower angles (0-20°) enabling long-distance multi-hop DX</li>
198
+ </ul>
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ <style>
204
+ /* 1. Backdrop Overlay */
205
+ .modal {
206
+ display: none;
207
+ position: fixed;
208
+ z-index: 1000;
209
+ left: 0;
210
+ top: 0;
211
+ width: 100%;
212
+ height: 100%;
213
+ background-color: rgba(0, 0, 0, 0.5); /* Dimmed background */
214
+ backdrop-filter: blur(4px); /* Modern blur effect */
215
+ display: flex; /* Centers the content easily */
216
+ align-items: center;
217
+ justify-content: center;
218
+ }
219
+
220
+ /* 2. The Modal Card */
221
+ .modal-card {
222
+ background-color: #ffffff;
223
+ padding: 2rem;
224
+ border-radius: 12px;
225
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
226
+ width: 90%;
227
+ max-width: 550px;
228
+ position: relative;
229
+ animation: modalFadeIn 0.3s ease-out;
230
+ }
231
+
232
+ /* 3. Typography & Spacing */
233
+ .help_title {
234
+ margin-top: 0;
235
+ color: #333;
236
+ font-family: sans-serif;
237
+ font-size: 1.5rem;
238
+ color: #070;
239
+ }
240
+
241
+ .modal-content {
242
+ line-height: 1.6;
243
+ color: #555;
244
+ font-family: sans-serif;
245
+ }
246
+
247
+ /* 4. Close Button */
248
+ .close {
249
+ position: absolute;
250
+ right: 20px;
251
+ top: 15px;
252
+ font-size: 1.8rem;
253
+ color: #999;
254
+ cursor: pointer;
255
+ transition: color 0.2s;
256
+ }
257
+
258
+ .close:hover {
259
+ color: #333;
260
+ }
261
+
262
+ /* 5. Simple Entrance Animation */
263
+ @keyframes modalFadeIn {
264
+ from { opacity: 0; transform: translateY(-20px); }
265
+ to { opacity: 1; transform: translateY(0); }
266
+ }
267
+ </style>
268
+
269
+ <script>
270
+ window.show_help = function(elemname) {
271
+ const modal = document.getElementById(elemname);
272
+ if (!modal) {
273
+ console.error("Modal element not found");
274
+ return;
275
+ }
276
+
277
+ modal.style.display = "flex";
278
+
279
+ const handleClosing = (event) => {
280
+ const isEscape = event.key === 'Escape';
281
+ const isClickOutside = event.target === modal;
282
+ const isCloseBtn = event.target.classList.contains('close');
283
+
284
+ if (isEscape || isClickOutside || isCloseBtn) {
285
+ modal.style.display = "none";
286
+ document.removeEventListener('keyup', handleClosing);
287
+ modal.removeEventListener('click', handleClosing);
288
+ }
289
+ };
290
+
291
+ document.addEventListener('keyup', handleClosing);
292
+ modal.addEventListener('click', handleClosing);
293
+ };
294
+ </script>
295
+
296
+ {% endblock %}