krushimitravit commited on
Commit
5e07b0a
·
verified ·
1 Parent(s): bceb09a

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +23 -0
  2. app.py +162 -0
  3. requirements.txt +5 -0
  4. templates/index.html +428 -0
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image
2
+ FROM python:3.9-slim
3
+
4
+ # Set the working directory
5
+ WORKDIR /app
6
+
7
+ # Copy only the requirements first (to leverage Docker cache)
8
+ COPY requirements.txt /app/requirements.txt
9
+
10
+ # Install dependencies
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy the rest of the application files
14
+ COPY . /app
15
+
16
+ # Create the directory for cached images
17
+ RUN mkdir -p cache_images
18
+
19
+ # Expose the port your app runs on
20
+ EXPOSE 7860
21
+
22
+ # Command to run the application
23
+ CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:7860", "app:app"]
app.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, Response
2
+ import requests
3
+ from bs4 import BeautifulSoup
4
+ from flask import stream_with_context
5
+
6
+ app = Flask(__name__)
7
+
8
+ # Internal mapping of crops to pests (for the form)
9
+ CROP_TO_PESTS = {
10
+ "Sorgum": ["FallArmyWorm"],
11
+ "Maize": ["FallArmyWorm"],
12
+ "Rice": ["Blast", "GallMidge", "YSB", "PlantHopper", "BlueBeetle", "BacterialLeafBlight"],
13
+ "Cotton": ["Thrips", "Whitefly", "PinkBollworm", "Jassid", "BollRot", "AmericanBollworm"],
14
+ "Soybean": ["Girdlebeetle", "H.armigera", "Semilooper", "Spodoptera", "StemFLy"],
15
+ "Tur": ["Wilt", "Webbed_Leaves", "Pod_damage"],
16
+ "Sugarcane": ["FallArmyGrub", "WhiteGrub"],
17
+ "Gram": ["H.armigera", "Wilt"]
18
+ }
19
+
20
+ # Fixed year options for the form
21
+ YEARS = ["2024-25", "2023-24", "2022-23", "2021-22"]
22
+
23
+ # Map our internal crop names to the external page's crop values.
24
+ CROP_MAPPING = {
25
+ "Cotton": "1",
26
+ "Gram": "4",
27
+ "Maize": "7",
28
+ "Rice": "3",
29
+ "Sorghum": "6",
30
+ "Soybean": "2",
31
+ "Sugarcane": "8",
32
+ "Tur": "5",
33
+ "Sorgum": "6" # Adjust if needed
34
+ }
35
+
36
+ # Map our internal pest names to external page values per crop.
37
+ PEST_MAPPING = {
38
+ "Cotton": {
39
+ "FallArmyWorm": "71"
40
+ },
41
+ "Gram": {
42
+ "H.armigera": "72",
43
+ "Wilt": "73"
44
+ },
45
+ "Maize": {
46
+ "FallArmyWorm": "74"
47
+ },
48
+ "Rice": {
49
+ "Blast": "75",
50
+ "GallMidge": "76",
51
+ "YSB": "77",
52
+ "PlantHopper": "78",
53
+ "BlueBeetle": "79",
54
+ "BacterialLeafBlight": "80"
55
+ },
56
+ "Soybean": {
57
+ "Girdlebeetle": "81",
58
+ "H.armigera": "82",
59
+ "Semilooper": "83",
60
+ "Spodoptera": "84",
61
+ "StemFLy": "85"
62
+ },
63
+ "Tur": {
64
+ "Wilt": "86",
65
+ "Webbed_Leaves": "87",
66
+ "Pod_damage": "88"
67
+ },
68
+ "Sugarcane": {
69
+ "FallArmyGrub": "89",
70
+ "WhiteGrub": "90"
71
+ },
72
+ "Sorgum": {
73
+ "FallArmyWorm": "91"
74
+ }
75
+ }
76
+
77
+ # Parameter codes and labels for the final image URL
78
+ PARAMS = {
79
+ "Mint": "Min Temperature",
80
+ "Maxt": "Max Temperature",
81
+ "RH": "Relative Humidity",
82
+ "RF": "Rainfall",
83
+ "PR": "Pest Report"
84
+ }
85
+
86
+ @app.route('/')
87
+ def index():
88
+ # Read query parameters (if provided)
89
+ crop = request.args.get('crop', '')
90
+ pest = request.args.get('pest', '')
91
+ year = request.args.get('year', '')
92
+ week = request.args.get('week', '')
93
+ param = request.args.get('param', '')
94
+
95
+ image_url = ""
96
+ if crop and pest and year and week and param:
97
+ # Build the external image URL (using HTTP)
98
+ base_url = f"http://www.icar-crida.res.in:8080/naip/gisimages/{crop}/{year}/{pest}_"
99
+ external_image_url = f"{base_url}{param}{week}.jpg"
100
+ # Instead of using the external HTTP URL directly, we build our proxy URL
101
+ image_url = f"/proxy-image?url={external_image_url}"
102
+
103
+ return render_template('index.html',
104
+ crops=list(CROP_TO_PESTS.keys()),
105
+ crop_to_pests=CROP_TO_PESTS,
106
+ years=YEARS,
107
+ params=PARAMS,
108
+ selected_crop=crop,
109
+ selected_pest=pest,
110
+ selected_year=year,
111
+ selected_week=week,
112
+ selected_param=param,
113
+ image_url=image_url)
114
+
115
+ @app.route('/fetch_weeks')
116
+ def fetch_weeks():
117
+ crop = request.args.get('crop', '')
118
+ pest = request.args.get('pest', '')
119
+ year = request.args.get('year', '')
120
+
121
+ ext_crop = CROP_MAPPING.get(crop, '')
122
+ ext_pest = ""
123
+ if crop in PEST_MAPPING and pest in PEST_MAPPING[crop]:
124
+ ext_pest = PEST_MAPPING[crop][pest]
125
+
126
+ payload = {
127
+ "country": ext_crop,
128
+ "city": ext_pest,
129
+ "sowing": year
130
+ }
131
+
132
+ weeks = []
133
+ try:
134
+ response = requests.get("http://www.icar-crida.res.in:8080/naip/gismaps.jsp", params=payload, timeout=10)
135
+ soup = BeautifulSoup(response.text, 'html.parser')
136
+ week_options = soup.select('select[name="week"] option')
137
+ weeks = [opt.get('value') for opt in week_options if opt.get('value') and "Select" not in opt.get('value')]
138
+ if not weeks:
139
+ weeks = [str(i) for i in range(1, 53)]
140
+ except Exception as e:
141
+ weeks = [str(i) for i in range(1, 53)]
142
+ return jsonify({"weeks": weeks})
143
+
144
+ @app.route('/proxy-image')
145
+ def proxy_image():
146
+ external_url = request.args.get('url')
147
+ if not external_url:
148
+ return "Missing URL", 400
149
+
150
+ try:
151
+ # Use streaming so that the response is sent in chunks
152
+ resp = requests.get(external_url, timeout=10, stream=True)
153
+ return Response(
154
+ stream_with_context(resp.iter_content(chunk_size=1024)),
155
+ mimetype=resp.headers.get('Content-Type', 'image/jpeg')
156
+ )
157
+ except Exception as e:
158
+ return str(e), 500
159
+
160
+
161
+ if __name__ == '__main__':
162
+ app.run(debug=True)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gunicorn
2
+ flask
3
+ beautifulsoup4
4
+ requests
5
+ flask_caching
templates/index.html ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>