krushimitravit commited on
Commit
fa5b595
Β·
verified Β·
1 Parent(s): 3942072

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +671 -428
templates/index.html CHANGED
@@ -1,428 +1,671 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Pest Geospatial Analytics | AgriTech Suite</title>
8
- <!-- Fonts -->
9
- <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
12
- <!-- Bootstrap -->
13
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
14
- <!-- Icons -->
15
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
16
-
17
- <style>
18
- :root {
19
- --primary-green: #1a5d3a;
20
- --accent-green: #198754;
21
- --light-bg: #f8f9fa;
22
- --text-dark: #212529;
23
- --card-shadow: 0 10px 40px rgba(0, 0, 0, 0.05);
24
- }
25
-
26
- body {
27
- font-family: 'Outfit', sans-serif;
28
- background-color: var(--light-bg);
29
- color: var(--text-dark);
30
- min-height: 100vh;
31
- display: flex;
32
- flex-direction: column;
33
- }
34
-
35
- /* --- Header Section --- */
36
- .analytics-header {
37
- background-color: var(--primary-green);
38
- color: white;
39
- padding: 3rem 1rem 6rem;
40
- text-align: center;
41
- border-bottom-left-radius: 50% 20px;
42
- border-bottom-right-radius: 50% 20px;
43
- margin-bottom: 2rem;
44
- }
45
-
46
- .header-title {
47
- font-weight: 700;
48
- font-size: 2rem;
49
- text-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
50
- }
51
-
52
- /* --- Main Dashboard --- */
53
- .dashboard-container {
54
- margin-top: -5rem;
55
- padding: 0 2rem 3rem;
56
- flex: 1;
57
- display: flex;
58
- justify-content: center;
59
- }
60
-
61
- .dashboard-card {
62
- background: white;
63
- border-radius: 24px;
64
- box-shadow: var(--card-shadow);
65
- width: 100%;
66
- max-width: 1200px;
67
- overflow: hidden;
68
- display: grid;
69
- grid-template-columns: 320px 1fr;
70
- /* Sidebar | Content */
71
- min-height: 600px;
72
- border: 1px solid #eef0f3;
73
- }
74
-
75
- /* --- Left Sidebar (Controls) --- */
76
- .sidebar-controls {
77
- background: #fdfdfd;
78
- border-right: 1px solid #f1f3f5;
79
- padding: 2rem;
80
- display: flex;
81
- flex-direction: column;
82
- }
83
-
84
- .sidebar-title {
85
- font-size: 1rem;
86
- font-weight: 700;
87
- color: var(--primary-green);
88
- margin-bottom: 2rem;
89
- display: flex;
90
- align-items: center;
91
- gap: 0.5rem;
92
- padding-bottom: 1rem;
93
- border-bottom: 2px solid #f0f0f0;
94
- }
95
-
96
- .form-group {
97
- margin-bottom: 1.5rem;
98
- }
99
-
100
- .form-label-custom {
101
- font-size: 0.8rem;
102
- font-weight: 600;
103
- color: #6c757d;
104
- text-transform: uppercase;
105
- letter-spacing: 0.5px;
106
- margin-bottom: 0.5rem;
107
- display: block;
108
- }
109
-
110
- .form-select-custom {
111
- width: 100%;
112
- border: 1px solid #e2e8f0;
113
- border-radius: 10px;
114
- padding: 0.8rem 1rem;
115
- font-size: 0.95rem;
116
- font-weight: 500;
117
- background-color: white;
118
- transition: all 0.2s;
119
- }
120
-
121
- .form-select-custom:focus {
122
- border-color: var(--accent-green);
123
- box-shadow: 0 0 0 4px rgba(25, 135, 84, 0.1);
124
- outline: none;
125
- }
126
-
127
- .btn-visualize {
128
- background-color: var(--accent-green);
129
- color: white;
130
- border: none;
131
- border-radius: 10px;
132
- padding: 1rem;
133
- font-weight: 600;
134
- width: 100%;
135
- display: flex;
136
- align-items: center;
137
- justify-content: center;
138
- gap: 0.5rem;
139
- margin-top: auto;
140
- /* Push to bottom */
141
- transition: all 0.2s;
142
- box-shadow: 0 4px 15px rgba(25, 135, 84, 0.2);
143
- }
144
-
145
- .btn-visualize:hover {
146
- background-color: #146c43;
147
- transform: translateY(-2px);
148
- box-shadow: 0 6px 20px rgba(25, 135, 84, 0.3);
149
- }
150
-
151
- /* --- Right Content (Map) --- */
152
- .map-viewport {
153
- background-color: #f8fafc;
154
- padding: 2rem;
155
- display: flex;
156
- flex-direction: column;
157
- align-items: center;
158
- justify-content: center;
159
- position: relative;
160
- }
161
-
162
- .map-frame {
163
- background: white;
164
- padding: 1rem;
165
- border-radius: 16px;
166
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
167
- max-width: 100%;
168
- }
169
-
170
- .map-image {
171
- max-width: 100%;
172
- max-height: 550px;
173
- border-radius: 8px;
174
- display: block;
175
- }
176
-
177
- .result-meta {
178
- position: absolute;
179
- top: 1.5rem;
180
- left: 1.5rem;
181
- background: white;
182
- padding: 0.5rem 1rem;
183
- border-radius: 50px;
184
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
185
- display: flex;
186
- gap: 1rem;
187
- z-index: 5;
188
- font-size: 0.85rem;
189
- font-weight: 500;
190
- color: var(--text-dark);
191
- }
192
-
193
- .meta-item {
194
- display: flex;
195
- align-items: center;
196
- gap: 0.5rem;
197
- }
198
-
199
- .meta-item i {
200
- color: var(--accent-green);
201
- }
202
-
203
- /* Empty States */
204
- .empty-state {
205
- text-align: center;
206
- color: #adb5bd;
207
- }
208
-
209
- .empty-icon {
210
- font-size: 4rem;
211
- margin-bottom: 1rem;
212
- color: #e9ecef;
213
- }
214
-
215
- /* Mobile */
216
- @media (max-width: 992px) {
217
- .dashboard-card {
218
- grid-template-columns: 1fr;
219
- }
220
-
221
- .sidebar-controls {
222
- border-right: none;
223
- border-bottom: 1px solid #f1f3f5;
224
- }
225
-
226
- .map-image {
227
- max-height: 400px;
228
- }
229
- }
230
-
231
- /* Loading Overlay */
232
- #loadingOverlay {
233
- position: absolute;
234
- inset: 0;
235
- background: rgba(255, 255, 255, 0.9);
236
- display: none;
237
- justify-content: center;
238
- align-items: center;
239
- z-index: 50;
240
- border-radius: 20px;
241
- }
242
- </style>
243
- </head>
244
-
245
- <body>
246
-
247
- <!-- Header -->
248
- <div class="analytics-header">
249
- <h1 class="header-title">Geospatial Intelligence Dashboard</h1>
250
- <p class="opacity-75">Analysis of pest distribution patterns over space and time</p>
251
- </div>
252
-
253
- <!-- Main Dashboard -->
254
- <div class="dashboard-container">
255
- <div class="dashboard-card">
256
-
257
- <!-- Sidebar: Controls -->
258
- <aside class="sidebar-controls">
259
- <div class="sidebar-title">
260
- <i class="bi bi-sliders2"></i> FILTER PARAMETERS
261
- </div>
262
-
263
- <form method="GET" action="/" id="analyticsForm">
264
-
265
- <div class="form-group">
266
- <label class="form-label-custom">Select Crop</label>
267
- <select id="crop" name="crop" class="form-select-custom" onchange="updatePestDropdown()">
268
- <option value="">Choose Crop...</option>
269
- {% for c in crops %}
270
- <option value="{{ c }}" {% if selected_crop==c %}selected{% endif %}>{{ c }}</option>
271
- {% endfor %}
272
- </select>
273
- </div>
274
-
275
- <div class="form-group">
276
- <label class="form-label-custom">Select Pest</label>
277
- <select id="pest" name="pest" class="form-select-custom">
278
- <option value="">Choose Pest...</option>
279
- <!-- JS Populated -->
280
- </select>
281
- </div>
282
-
283
- <div class="row">
284
- <div class="col-6 form-group">
285
- <label class="form-label-custom">Year</label>
286
- <select id="year" name="year" class="form-select-custom" onchange="fetchWeeks()">
287
- <option value="">Year...</option>
288
- {% for y in years %}
289
- <option value="{{ y }}" {% if selected_year==y %}selected{% endif %}>{{ y }}</option>
290
- {% endfor %}
291
- </select>
292
- </div>
293
- <div class="col-6 form-group">
294
- <label class="form-label-custom">Week</label>
295
- <select id="week" name="week" class="form-select-custom">
296
- <option value="">Week...</option>
297
- {% if selected_week %}
298
- <option value="{{ selected_week }}" selected>{{ selected_week }}</option>
299
- {% endif %}
300
- </select>
301
- </div>
302
- </div>
303
-
304
- <div class="form-group mb-5">
305
- <label class="form-label-custom">Analysis Metric</label>
306
- <select id="param" name="param" class="form-select-custom">
307
- <option value="">Select Metric...</option>
308
- {% for code, label in params.items() %}
309
- <option value="{{ code }}" {% if selected_param==code %}selected{% endif %}>{{ label }}</option>
310
- {% endfor %}
311
- </select>
312
- </div>
313
-
314
- <button type="submit" class="btn-visualize">
315
- Generate Map <i class="bi bi-arrow-right-circle"></i>
316
- </button>
317
-
318
- </form>
319
- </aside>
320
-
321
- <!-- Main: Visualization -->
322
- <main class="map-viewport">
323
-
324
- <!-- Loading -->
325
- <div id="loadingOverlay">
326
- <div class="text-center">
327
- <div class="spinner-border text-success mb-3" role="status"></div>
328
- <h5 class="text-muted">Rendering Geospatial Data...</h5>
329
- </div>
330
- </div>
331
-
332
- {% if image_url %}
333
- <!-- Results Header -->
334
- <div class="result-meta">
335
- <div class="meta-item"><i class="bi bi-calendar-event"></i> {{ selected_year }} (W{{ selected_week }})</div>
336
- <div class="d-none d-md-flex meta-item text-muted">|</div>
337
- <div class="meta-item"><i class="bi bi-bug"></i> {{ selected_pest }}</div>
338
- </div>
339
-
340
- <div class="map-frame" id="mapContainer">
341
- <img src="{{ image_url }}" alt="Heatmap Result" class="map-image" onerror="handleImageError(this)">
342
-
343
- <!-- Error State (Hidden by default) -->
344
- <div id="dataNotAvailable" class="p-5 text-center d-none">
345
- <i class="bi bi-database-x fs-1 text-danger mb-3"></i>
346
- <h5 class="text-secondary">Data Unavailable</h5>
347
- <p class="text-muted small">No records found for this parameter combination.</p>
348
- </div>
349
- </div>
350
-
351
- {% else %}
352
- <!-- Empty State -->
353
- <div class="empty-state">
354
- <div class="empty-icon"><i class="bi bi-map"></i></div>
355
- <h4>Map Visualization</h4>
356
- <p>Configure filters on the left to generate insights.</p>
357
- </div>
358
- {% endif %}
359
-
360
- </main>
361
-
362
- </div>
363
- </div>
364
-
365
- <!-- Scripts -->
366
- <script>
367
- const cropToPests = {{ crop_to_pests | tojson }};
368
-
369
- function updatePestDropdown() {
370
- const cropSelect = document.getElementById("crop");
371
- const pestSelect = document.getElementById("pest");
372
- const selectedCrop = cropSelect.value;
373
- const currentPest = "{{ selected_pest }}";
374
-
375
- pestSelect.innerHTML = '<option value="">Choose Pest...</option>';
376
-
377
- if (selectedCrop && cropToPests[selectedCrop]) {
378
- cropToPests[selectedCrop].forEach(p => {
379
- const opt = document.createElement("option");
380
- opt.value = p; opt.textContent = p;
381
- if (p === currentPest) opt.selected = true;
382
- pestSelect.appendChild(opt);
383
- });
384
- }
385
- if (document.getElementById("year").value) fetchWeeks();
386
- }
387
-
388
- function fetchWeeks() {
389
- const crop = document.getElementById("crop").value;
390
- const pest = document.getElementById("pest").value;
391
- const year = document.getElementById("year").value;
392
- const currentWeek = "{{ selected_week }}";
393
-
394
- if (!crop || !pest || !year) return;
395
-
396
- fetch(`/fetch_weeks?crop=${crop}&pest=${pest}&year=${year}`)
397
- .then(res => res.json())
398
- .then(data => {
399
- const weekSelect = document.getElementById("week");
400
- weekSelect.innerHTML = '<option value="">Week...</option>';
401
- data.weeks.forEach(w => {
402
- const opt = document.createElement("option");
403
- opt.value = w; opt.textContent = w;
404
- if (w == currentWeek) opt.selected = true;
405
- weekSelect.appendChild(opt);
406
- });
407
- });
408
- }
409
-
410
- function handleImageError(img) {
411
- img.style.display = 'none';
412
- document.getElementById('dataNotAvailable').classList.remove('d-none');
413
- }
414
-
415
- document.getElementById('analyticsForm').addEventListener('submit', () => {
416
- document.getElementById('loadingOverlay').style.display = 'flex';
417
- });
418
-
419
- window.onload = () => {
420
- updatePestDropdown();
421
- if ("{{ selected_year }}" && "{{ selected_crop }}") fetchWeeks();
422
- };
423
- </script>
424
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
425
-
426
- </body>
427
-
428
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Pest Geospatial Analytics | AgriTech Suite</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
13
+
14
+ <style>
15
+ :root {
16
+ --primary-green: #1a5d3a;
17
+ --accent-green: #198754;
18
+ --light-bg: #f8f9fa;
19
+ --text-dark: #212529;
20
+ --card-shadow: 0 10px 40px rgba(0, 0, 0, 0.05);
21
+ }
22
+
23
+ body {
24
+ font-family: 'Outfit', sans-serif;
25
+ background-color: var(--light-bg);
26
+ color: var(--text-dark);
27
+ min-height: 100vh;
28
+ display: flex;
29
+ flex-direction: column;
30
+ }
31
+
32
+ .analytics-header {
33
+ background-color: var(--primary-green);
34
+ color: white;
35
+ padding: 3rem 1rem 6rem;
36
+ text-align: center;
37
+ border-bottom-left-radius: 50% 20px;
38
+ border-bottom-right-radius: 50% 20px;
39
+ margin-bottom: 2rem;
40
+ }
41
+
42
+ .header-title {
43
+ font-weight: 700;
44
+ font-size: 2rem;
45
+ text-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
46
+ }
47
+
48
+ .dashboard-container {
49
+ margin-top: -5rem;
50
+ padding: 0 2rem 3rem;
51
+ flex: 1;
52
+ display: flex;
53
+ justify-content: center;
54
+ }
55
+
56
+ .dashboard-card {
57
+ background: white;
58
+ border-radius: 24px;
59
+ box-shadow: var(--card-shadow);
60
+ width: 100%;
61
+ max-width: 1200px;
62
+ overflow: hidden;
63
+ display: grid;
64
+ grid-template-columns: 320px 1fr;
65
+ min-height: 600px;
66
+ border: 1px solid #eef0f3;
67
+ }
68
+
69
+ .sidebar-controls {
70
+ background: #fdfdfd;
71
+ border-right: 1px solid #f1f3f5;
72
+ padding: 2rem;
73
+ display: flex;
74
+ flex-direction: column;
75
+ }
76
+
77
+ .sidebar-title {
78
+ font-size: 1rem;
79
+ font-weight: 700;
80
+ color: var(--primary-green);
81
+ margin-bottom: 2rem;
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 0.5rem;
85
+ padding-bottom: 1rem;
86
+ border-bottom: 2px solid #f0f0f0;
87
+ }
88
+
89
+ .form-group {
90
+ margin-bottom: 1.5rem;
91
+ }
92
+
93
+ .form-label-custom {
94
+ font-size: 0.8rem;
95
+ font-weight: 600;
96
+ color: #6c757d;
97
+ text-transform: uppercase;
98
+ letter-spacing: 0.5px;
99
+ margin-bottom: 0.5rem;
100
+ display: block;
101
+ }
102
+
103
+ .form-select-custom {
104
+ width: 100%;
105
+ border: 1px solid #e2e8f0;
106
+ border-radius: 10px;
107
+ padding: 0.8rem 1rem;
108
+ font-size: 0.95rem;
109
+ font-weight: 500;
110
+ background-color: white;
111
+ transition: all 0.2s;
112
+ }
113
+
114
+ .form-select-custom:focus {
115
+ border-color: var(--accent-green);
116
+ box-shadow: 0 0 0 4px rgba(25, 135, 84, 0.1);
117
+ outline: none;
118
+ }
119
+
120
+ .form-select-custom option:disabled {
121
+ color: #adb5bd;
122
+ background-color: #f8f9fa;
123
+ }
124
+
125
+ .btn-visualize {
126
+ background-color: var(--accent-green);
127
+ color: white;
128
+ border: none;
129
+ border-radius: 10px;
130
+ padding: 1rem;
131
+ font-weight: 600;
132
+ width: 100%;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ gap: 0.5rem;
137
+ margin-top: auto;
138
+ transition: all 0.2s;
139
+ box-shadow: 0 4px 15px rgba(25, 135, 84, 0.2);
140
+ }
141
+
142
+ .btn-visualize:hover:not(:disabled) {
143
+ background-color: #146c43;
144
+ transform: translateY(-2px);
145
+ box-shadow: 0 6px 20px rgba(25, 135, 84, 0.3);
146
+ }
147
+
148
+ .btn-visualize:disabled {
149
+ opacity: 0.55;
150
+ cursor: not-allowed;
151
+ }
152
+
153
+ .map-viewport {
154
+ background-color: #f8fafc;
155
+ padding: 2rem;
156
+ display: flex;
157
+ flex-direction: column;
158
+ align-items: center;
159
+ justify-content: center;
160
+ position: relative;
161
+ }
162
+
163
+ .map-frame {
164
+ background: white;
165
+ padding: 1rem;
166
+ border-radius: 16px;
167
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
168
+ max-width: 100%;
169
+ }
170
+
171
+ .map-image {
172
+ max-width: 100%;
173
+ max-height: 550px;
174
+ border-radius: 8px;
175
+ display: block;
176
+ }
177
+
178
+ .result-meta {
179
+ position: absolute;
180
+ top: 1.5rem;
181
+ left: 1.5rem;
182
+ background: white;
183
+ padding: 0.5rem 1rem;
184
+ border-radius: 50px;
185
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
186
+ display: flex;
187
+ gap: 1rem;
188
+ z-index: 5;
189
+ font-size: 0.85rem;
190
+ font-weight: 500;
191
+ color: var(--text-dark);
192
+ }
193
+
194
+ .meta-item {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 0.5rem;
198
+ }
199
+
200
+ .meta-item i {
201
+ color: var(--accent-green);
202
+ }
203
+
204
+ .empty-state {
205
+ text-align: center;
206
+ color: #adb5bd;
207
+ }
208
+
209
+ .empty-icon {
210
+ font-size: 4rem;
211
+ margin-bottom: 1rem;
212
+ color: #e9ecef;
213
+ }
214
+
215
+ /* Status badges inside dropdowns */
216
+ .week-loading-badge {
217
+ display: inline-flex;
218
+ align-items: center;
219
+ gap: 0.4rem;
220
+ font-size: 0.75rem;
221
+ color: #6c757d;
222
+ margin-top: 0.3rem;
223
+ }
224
+
225
+ .availability-hint {
226
+ font-size: 0.72rem;
227
+ color: #adb5bd;
228
+ margin-top: 0.25rem;
229
+ }
230
+
231
+ @media (max-width: 992px) {
232
+ .dashboard-card {
233
+ grid-template-columns: 1fr;
234
+ }
235
+
236
+ .sidebar-controls {
237
+ border-right: none;
238
+ border-bottom: 1px solid #f1f3f5;
239
+ }
240
+
241
+ .map-image {
242
+ max-height: 400px;
243
+ }
244
+ }
245
+
246
+ #loadingOverlay {
247
+ position: absolute;
248
+ inset: 0;
249
+ background: rgba(255, 255, 255, 0.9);
250
+ display: none;
251
+ justify-content: center;
252
+ align-items: center;
253
+ z-index: 50;
254
+ border-radius: 20px;
255
+ }
256
+
257
+ /* Spinner inside dropdowns while checking */
258
+ .select-spinner {
259
+ position: relative;
260
+ }
261
+
262
+ .select-spinner::after {
263
+ content: '';
264
+ position: absolute;
265
+ right: 2.5rem;
266
+ top: 50%;
267
+ transform: translateY(-50%);
268
+ width: 14px;
269
+ height: 14px;
270
+ border: 2px solid #dee2e6;
271
+ border-top-color: var(--accent-green);
272
+ border-radius: 50%;
273
+ animation: spin 0.6s linear infinite;
274
+ }
275
+
276
+ @keyframes spin {
277
+ from { transform: translateY(-50%) rotate(0deg); }
278
+ to { transform: translateY(-50%) rotate(360deg); }
279
+ }
280
+ </style>
281
+ </head>
282
+
283
+ <body>
284
+
285
+ <div class="analytics-header">
286
+ <h1 class="header-title">Geospatial Intelligence Dashboard</h1>
287
+ <p class="opacity-75">Analysis of pest distribution patterns over space and time</p>
288
+ </div>
289
+
290
+ <div class="dashboard-container">
291
+ <div class="dashboard-card">
292
+
293
+ <aside class="sidebar-controls">
294
+ <div class="sidebar-title">
295
+ <i class="bi bi-sliders2"></i> FILTER PARAMETERS
296
+ </div>
297
+
298
+ <form method="GET" action="/" id="analyticsForm">
299
+
300
+ <div class="form-group">
301
+ <label class="form-label-custom">Select Crop</label>
302
+ <select id="crop" name="crop" class="form-select-custom" onchange="onCropChange('')">
303
+ <option value="">Choose Crop...</option>
304
+ {% for c in crops %}
305
+ <option value="{{ c }}" {% if selected_crop==c %}selected{% endif %}>{{ c }}</option>
306
+ {% endfor %}
307
+ </select>
308
+ </div>
309
+
310
+ <div class="form-group">
311
+ <label class="form-label-custom">Select Pest</label>
312
+ <select id="pest" name="pest" class="form-select-custom" onchange="onPestOrYearChange()">
313
+ <option value="">Choose Pest...</option>
314
+ </select>
315
+ </div>
316
+
317
+ <div class="row">
318
+ <div class="col-6 form-group">
319
+ <label class="form-label-custom">Year</label>
320
+ <select id="year" name="year" class="form-select-custom" onchange="onPestOrYearChange()">
321
+ <option value="">Year...</option>
322
+ {% for y in years %}
323
+ <option value="{{ y }}" {% if selected_year==y %}selected{% endif %}>{{ y }}</option>
324
+ {% endfor %}
325
+ </select>
326
+ </div>
327
+ <div class="col-6 form-group">
328
+ <label class="form-label-custom">Week</label>
329
+ <select id="week" name="week" class="form-select-custom" onchange="onWeekChange()">
330
+ <option value="">Week...</option>
331
+ {% if selected_week %}
332
+ <option value="{{ selected_week }}" selected>Week {{ selected_week }}</option>
333
+ {% endif %}
334
+ </select>
335
+ <span id="weekHint" class="availability-hint"></span>
336
+ </div>
337
+ </div>
338
+
339
+ <div class="form-group mb-5">
340
+ <label class="form-label-custom">Analysis Metric</label>
341
+ <select id="param" name="param" class="form-select-custom">
342
+ <option value="">Select Metric...</option>
343
+ {% for code, label in params.items() %}
344
+ <option value="{{ code }}" {% if selected_param==code %}selected{% endif %}>{{ label }}</option>
345
+ {% endfor %}
346
+ </select>
347
+ <span id="paramHint" class="availability-hint"></span>
348
+ </div>
349
+
350
+ <button type="submit" class="btn-visualize" id="submitBtn">
351
+ Generate Map <i class="bi bi-arrow-right-circle"></i>
352
+ </button>
353
+
354
+ </form>
355
+ </aside>
356
+
357
+ <main class="map-viewport">
358
+
359
+ <div id="loadingOverlay">
360
+ <div class="text-center">
361
+ <div class="spinner-border text-success mb-3" role="status"></div>
362
+ <h5 class="text-muted">Rendering Geospatial Data...</h5>
363
+ </div>
364
+ </div>
365
+
366
+ {% if image_url %}
367
+ <div class="result-meta">
368
+ <div class="meta-item"><i class="bi bi-calendar-event"></i> {{ selected_year }} (W{{ selected_week }})</div>
369
+ <div class="d-none d-md-flex meta-item text-muted">|</div>
370
+ <div class="meta-item"><i class="bi bi-bug"></i> {{ selected_pest }}</div>
371
+ </div>
372
+
373
+ <div class="map-frame" id="mapContainer">
374
+ <img src="{{ image_url }}" alt="Heatmap Result" class="map-image" onerror="handleImageError(this)">
375
+ <div id="dataNotAvailable" class="p-5 text-center d-none">
376
+ <i class="bi bi-database-x fs-1 text-danger mb-3"></i>
377
+ <h5 class="text-secondary">Data Unavailable</h5>
378
+ <p class="text-muted small">No records found for this parameter combination.</p>
379
+ </div>
380
+ </div>
381
+
382
+ {% elif not data_available %}
383
+ <div class="empty-state">
384
+ <div class="empty-icon"><i class="bi bi-database-x text-danger"></i></div>
385
+ <h4 class="text-secondary">Data Unavailable</h4>
386
+ <p class="text-muted">No image data found for the selected combination.<br>Try a different week, year, or metric.</p>
387
+ </div>
388
+
389
+ {% else %}
390
+ <div class="empty-state">
391
+ <div class="empty-icon"><i class="bi bi-map"></i></div>
392
+ <h4>Map Visualization</h4>
393
+ <p>Configure filters on the left to generate insights.</p>
394
+ </div>
395
+ {% endif %}
396
+
397
+ </main>
398
+
399
+ </div>
400
+ </div>
401
+
402
+ <!-- Server-side data passed via data attributes β€” keeps <script> Jinja-free -->
403
+ <div id="appData"
404
+ data-crop-to-pests="{{ crop_to_pests | tojson | e }}"
405
+ data-init-crop="{{ selected_crop | e }}"
406
+ data-init-pest="{{ selected_pest | e }}"
407
+ data-init-year="{{ selected_year | e }}"
408
+ data-init-week="{{ selected_week | e }}"
409
+ data-init-param="{{ selected_param | e }}"
410
+ style="display:none"
411
+ ></div>
412
+
413
+ <script>
414
+ const _appData = document.getElementById('appData').dataset;
415
+ const cropToPests = JSON.parse(_appData.cropToPests);
416
+
417
+ // Server-rendered selections (used to restore state after JS rebuilds dropdowns)
418
+ const INIT_CROP = _appData.initCrop;
419
+ const INIT_PEST = _appData.initPest;
420
+ const INIT_YEAR = _appData.initYear;
421
+ const INIT_WEEK = _appData.initWeek;
422
+ const INIT_PARAM = _appData.initParam;
423
+
424
+ // Track in-flight availability request so we can cancel stale results
425
+ let availabilityController = null;
426
+
427
+ // ── Crop changed β†’ repopulate pest dropdown ──────────────────────────────
428
+ function onCropChange(restorePest) {
429
+ const crop = document.getElementById("crop").value;
430
+ const pestSelect = document.getElementById("pest");
431
+
432
+ pestSelect.innerHTML = '<option value="">Choose Pest...</option>';
433
+
434
+ if (crop && cropToPests[crop]) {
435
+ cropToPests[crop].forEach(p => {
436
+ const opt = document.createElement("option");
437
+ opt.value = p;
438
+ opt.textContent = p;
439
+ pestSelect.appendChild(opt);
440
+ });
441
+ }
442
+
443
+ // Restore a previously selected pest if requested (page-load restore)
444
+ if (restorePest && restorePest !== "") {
445
+ pestSelect.value = restorePest;
446
+ }
447
+
448
+ clearWeeks();
449
+ clearParamAvailability();
450
+
451
+ // Only fetch weeks if both pest and year are already set
452
+ const year = document.getElementById("year").value;
453
+ if (crop && pestSelect.value && year) {
454
+ fetchWeeks();
455
+ }
456
+ }
457
+
458
+ // ── Pest or Year changed β†’ refresh weeks ─────────────────────────────────
459
+ function onPestOrYearChange() {
460
+ clearWeeks();
461
+ clearParamAvailability();
462
+ const crop = document.getElementById("crop").value;
463
+ const pest = document.getElementById("pest").value;
464
+ const year = document.getElementById("year").value;
465
+ if (crop && pest && year) fetchWeeks();
466
+ }
467
+
468
+ // ── Week changed β†’ check param availability ───────────────────────────────
469
+ function onWeekChange() {
470
+ clearParamAvailability();
471
+ const crop = document.getElementById("crop").value;
472
+ const pest = document.getElementById("pest").value;
473
+ const year = document.getElementById("year").value;
474
+ const week = document.getElementById("week").value;
475
+ if (crop && pest && year && week) checkParamAvailability(crop, pest, year, week);
476
+ }
477
+
478
+ // ── Fetch available weeks from backend ────────────────────────────────────
479
+ function fetchWeeks(restoreWeek) {
480
+ const crop = document.getElementById("crop").value;
481
+ const pest = document.getElementById("pest").value;
482
+ const year = document.getElementById("year").value;
483
+
484
+ const weekSelect = document.getElementById("week");
485
+ const hint = document.getElementById("weekHint");
486
+
487
+ weekSelect.innerHTML = '<option value="">Checking availability...</option>';
488
+ weekSelect.disabled = true;
489
+ hint.textContent = "Scanning weeks with data...";
490
+ hint.style.color = "#6c757d";
491
+
492
+ fetch(`/fetch_weeks?crop=${encodeURIComponent(crop)}&pest=${encodeURIComponent(pest)}&year=${encodeURIComponent(year)}`)
493
+ .then(res => {
494
+ if (!res.ok) throw new Error("Network response was not ok");
495
+ return res.json();
496
+ })
497
+ .then(data => {
498
+ weekSelect.innerHTML = '<option value="">Week...</option>';
499
+ weekSelect.disabled = false;
500
+
501
+ const weeks = Array.isArray(data.weeks) ? data.weeks : [];
502
+
503
+ if (weeks.length > 0) {
504
+ weeks.forEach(w => {
505
+ const opt = document.createElement("option");
506
+ opt.value = String(w);
507
+ opt.textContent = `Week ${w}`;
508
+ weekSelect.appendChild(opt);
509
+ });
510
+ hint.textContent = `${weeks.length} week(s) with data available`;
511
+ hint.style.color = "#198754";
512
+
513
+ // Restore a previously selected week (page-load) or keep current selection
514
+ const target = restoreWeek || INIT_WEEK;
515
+ if (target !== "" && weeks.map(String).includes(String(target))) {
516
+ weekSelect.value = String(target);
517
+ }
518
+ } else {
519
+ hint.textContent = "No data found for this combination";
520
+ hint.style.color = "#dc3545";
521
+ }
522
+
523
+ // If a week ended up selected, check param availability
524
+ if (weekSelect.value) {
525
+ checkParamAvailability(crop, pest, year, weekSelect.value);
526
+ }
527
+ })
528
+ .catch(() => {
529
+ weekSelect.innerHTML = '<option value="">Week...</option>';
530
+ weekSelect.disabled = false;
531
+ hint.textContent = "Could not load weeks β€” check connection";
532
+ hint.style.color = "#dc3545";
533
+ });
534
+ }
535
+
536
+ // ── Check which params are available for selected combination ─────────────
537
+ function checkParamAvailability(crop, pest, year, week) {
538
+ const paramSelect = document.getElementById("param");
539
+ const hint = document.getElementById("paramHint");
540
+ const submitBtn = document.getElementById("submitBtn");
541
+
542
+ // Abort any previous in-flight request
543
+ if (availabilityController) availabilityController.abort();
544
+ availabilityController = new AbortController();
545
+
546
+ hint.textContent = "Checking metric availability...";
547
+ hint.style.color = "#6c757d";
548
+ submitBtn.disabled = true;
549
+
550
+ fetch(
551
+ `/check_availability?crop=${encodeURIComponent(crop)}&pest=${encodeURIComponent(pest)}&year=${encodeURIComponent(year)}&week=${encodeURIComponent(week)}`,
552
+ { signal: availabilityController.signal }
553
+ )
554
+ .then(res => {
555
+ if (!res.ok) throw new Error("Network response was not ok");
556
+ return res.json();
557
+ })
558
+ .then(data => {
559
+ availabilityController = null;
560
+ const availability = data.availability || {};
561
+ let availableCount = 0;
562
+
563
+ Array.from(paramSelect.options).forEach(opt => {
564
+ if (!opt.value) return; // skip placeholder option
565
+
566
+ const isAvailable = availability[opt.value] === true;
567
+ opt.disabled = !isAvailable;
568
+
569
+ // Keep label clean β€” strip any stale "(unavailable)" suffix first
570
+ opt.textContent = opt.textContent.replace(/\s*\(unavailable\)$/, "");
571
+
572
+ if (isAvailable) {
573
+ availableCount++;
574
+ } else {
575
+ opt.textContent += " (unavailable)";
576
+ // Auto-deselect if this option was selected
577
+ if (opt.selected) {
578
+ opt.selected = false;
579
+ }
580
+ }
581
+ });
582
+
583
+ // If the previously selected param is now deselected, reset to placeholder
584
+ if (paramSelect.selectedIndex > 0 && paramSelect.options[paramSelect.selectedIndex].disabled) {
585
+ paramSelect.value = "";
586
+ }
587
+
588
+ if (availableCount === 0) {
589
+ hint.textContent = "No metrics available for this week";
590
+ hint.style.color = "#dc3545";
591
+ submitBtn.disabled = true;
592
+ } else {
593
+ hint.textContent = `${availableCount} metric(s) available`;
594
+ hint.style.color = "#198754";
595
+ submitBtn.disabled = false;
596
+ }
597
+ })
598
+ .catch(err => {
599
+ if (err.name === "AbortError") return; // stale request β€” ignore
600
+ availabilityController = null;
601
+ hint.textContent = "";
602
+ submitBtn.disabled = false;
603
+ });
604
+ }
605
+
606
+ // ── Helpers ───────────────────────────────────────────────────────────────
607
+ function clearWeeks() {
608
+ const weekSelect = document.getElementById("week");
609
+ weekSelect.innerHTML = '<option value="">Week...</option>';
610
+ weekSelect.disabled = false;
611
+ const hint = document.getElementById("weekHint");
612
+ hint.textContent = "";
613
+ }
614
+
615
+ function clearParamAvailability() {
616
+ // Abort any in-flight availability check
617
+ if (availabilityController) {
618
+ availabilityController.abort();
619
+ availabilityController = null;
620
+ }
621
+ const paramSelect = document.getElementById("param");
622
+ Array.from(paramSelect.options).forEach(opt => {
623
+ opt.disabled = false;
624
+ opt.textContent = opt.textContent.replace(/\s*\(unavailable\)$/, "");
625
+ });
626
+ document.getElementById("paramHint").textContent = "";
627
+ document.getElementById("submitBtn").disabled = false;
628
+ }
629
+
630
+ function handleImageError(img) {
631
+ img.style.display = 'none';
632
+ const unavailableEl = document.getElementById('dataNotAvailable');
633
+ if (unavailableEl) unavailableEl.classList.remove('d-none');
634
+ }
635
+
636
+ // ── Form submit guard ─────────────────────────────────────────────────────
637
+ document.getElementById('analyticsForm').addEventListener('submit', (e) => {
638
+ const fields = ['crop', 'pest', 'year', 'week', 'param'];
639
+ const missing = fields.filter(id => !document.getElementById(id).value);
640
+ if (missing.length > 0) {
641
+ e.preventDefault();
642
+ alert(`Please select: ${missing.join(', ')}`);
643
+ return;
644
+ }
645
+ document.getElementById('loadingOverlay').style.display = 'flex';
646
+ });
647
+
648
+ // ── Page-load restore ─────────────────────────────────────────────────────
649
+ window.addEventListener('DOMContentLoaded', () => {
650
+ // Step 1: restore crop selection (already set by server-rendered HTML)
651
+ // Step 2: rebuild pest dropdown and restore pest selection
652
+ onCropChange(INIT_PEST);
653
+
654
+ // Step 3: restore year (already set by server-rendered HTML)
655
+ // Step 4: fetch weeks and restore week + param selections
656
+ if (INIT_CROP && INIT_PEST && INIT_YEAR) {
657
+ fetchWeeks(INIT_WEEK);
658
+ }
659
+
660
+ // Restore param selection after availability check completes (handled inside fetchWeeks β†’ checkParamAvailability)
661
+ // But if no week is set yet, at least restore the param visually
662
+ if (INIT_PARAM) {
663
+ document.getElementById("param").value = INIT_PARAM;
664
+ }
665
+ });
666
+ </script>
667
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
668
+
669
+ </body>
670
+
671
+ </html>