atsuga commited on
Commit
97c76e2
·
verified ·
1 Parent(s): 2d518ce

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +684 -684
templates/index.html CHANGED
@@ -1,684 +1,684 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Madura weather</title>
5
- <meta charset="utf-8" />
6
- <link
7
- rel="stylesheet"
8
- href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
9
- />
10
-
11
- <style>
12
- body {
13
- font-family: Arial, sans-serif;
14
- margin: 0;
15
- padding: 0;
16
- min-height: 100vh;
17
- background-color: #f4f4f4;
18
- display: flex; /* Menggunakan flexbox untuk konten utama */
19
- flex-direction: column;
20
- }
21
- html,
22
- body,
23
- #map {
24
- height: 100%;
25
- margin: 0;
26
- padding: 0;
27
- }
28
-
29
- /* ================= LEGEND ================= */
30
- /* Mengubah posisi z-index untuk mencegah tumpang tindih */
31
- .legend {
32
- position: absolute;
33
- bottom: 30px;
34
- left: 20px;
35
- background: white;
36
- padding: 10px;
37
- line-height: 1.5;
38
- border-radius: 6px;
39
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
40
- font-size: 13px;
41
- z-index: 9999;
42
- }
43
- .legend h4 {
44
- margin: 0 0 5px 0;
45
- font-size: 14px;
46
- font-weight: bold;
47
- }
48
- .legend .item {
49
- display: flex;
50
- align-items: flex-start;
51
- gap: 6px;
52
- margin-bottom: 5px;
53
- }
54
- .legend .swatch {
55
- width: 18px;
56
- height: 12px;
57
- border: 1px solid #444;
58
- flex-shrink: 0;
59
- margin-top: 3px;
60
- }
61
- .legend .text-content {
62
- display: flex;
63
- flex-direction: column;
64
- text-align: left;
65
- }
66
-
67
- /* ================= NAVBAR ================= */
68
- .navbar {
69
- display: flex;
70
- justify-content: space-between;
71
- align-items: center;
72
- background: #1a73e8;
73
- padding: 10px 20px;
74
- color: white;
75
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
76
- position: relative;
77
- z-index: 10000;
78
- }
79
- .navbar .brand {
80
- font-size: 18px;
81
- font-weight: bold;
82
- }
83
- .navbar .menu {
84
- list-style: none;
85
- display: flex;
86
- gap: 20px;
87
- margin: 0;
88
- padding: 0;
89
- }
90
- .navbar .menu li a {
91
- color: white;
92
- text-decoration: none;
93
- }
94
-
95
- /* ================= CLUSTER CONTROL ================= */
96
- .clustering-controls {
97
- position: absolute;
98
- top: 80px;
99
- left: 20px;
100
- width: 300px;
101
- background: white;
102
- border-radius: 10px;
103
- padding: 15px;
104
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
105
- z-index: 9999;
106
- }
107
- .clustering-controls h3 {
108
- color: #1a73e8;
109
- margin-top: 0;
110
- border-bottom: 1px solid #eee;
111
- padding-bottom: 8px;
112
- }
113
- .clustering-controls select,
114
- .clustering-controls input[type="number"] {
115
- width: 100%;
116
- padding: 8px;
117
- margin-bottom: 10px;
118
- border: 1px solid #ccc;
119
- }
120
- .clustering-controls button {
121
- width: 100%;
122
- padding: 10px;
123
- background: #4caf50;
124
- border: none;
125
- border-radius: 5px;
126
- color: white;
127
- font-weight: bold;
128
- cursor: pointer;
129
- }
130
- .clustering-controls button:hover {
131
- background: #45a049;
132
- }
133
-
134
- #notificationBox {
135
- margin-top: 15px;
136
- padding: 10px;
137
- border-radius: 4px;
138
- font-size: 12px;
139
- }
140
-
141
- /* ================= TABEL INTERPRETASI ================= */
142
- #interpretasiKlaster {
143
- width: 100%;
144
- margin-top: 15px;
145
- border-collapse: collapse;
146
- font-size: 11px;
147
- display: none;
148
- }
149
- #interpretasiKlaster th,
150
- #interpretasiKlaster td {
151
- border: 1px solid #ddd;
152
- padding: 6px;
153
- text-align: center;
154
- }
155
- #interpretasiKlaster th {
156
- background: #f2f2f2;
157
- }
158
-
159
- /* ================= CHART BOX ================= */
160
- #chartBox {
161
- position: absolute;
162
- top: 80px;
163
- right: 20px;
164
- width: 450px;
165
- background: white;
166
- border-radius: 10px;
167
- padding: 15px;
168
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
169
- z-index: 9999;
170
- display: none;
171
- }
172
- </style>
173
-
174
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
175
- <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
176
- </head>
177
-
178
- <body>
179
- <header>
180
- <nav class="navbar">
181
- <div class="brand">Madura Weather</div>
182
- <ul class="menu">
183
- <li><a href="/">Homepage</a></li>
184
- <li><a href="/crop">Recomendation Plant</a></li>
185
- <li><a href="/cuaca">Image Prediction</a></li>
186
- <li><a href="/about">About</a></li>
187
- </ul>
188
- </nav>
189
- </header>
190
-
191
- <div id="map">
192
- <button
193
- onclick="toggleControls(true)"
194
- style="
195
- position: absolute;
196
- top: 80px;
197
- left: 20px;
198
- z-index: 10001;
199
- padding: 5px 10px;
200
- background: #1a73e8;
201
- color: white;
202
- border: none;
203
- border-radius: 5px;
204
- cursor: pointer;
205
- "
206
- id="showControlsBtn"
207
- >
208
- ▶ Control
209
- </button>
210
-
211
- <div
212
- class="clustering-controls"
213
- id="clusteringControlsPanel"
214
- style="display: none"
215
- >
216
- <div
217
- style="
218
- display: flex;
219
- justify-content: space-between;
220
- align-items: center;
221
- "
222
- >
223
- <h3>Clustering Simulation (K-Means)</h3>
224
- <button
225
- onclick="toggleControls(false)"
226
- style="
227
- border: none;
228
- background: none;
229
- font-size: 20px;
230
- cursor: pointer;
231
- color: #1a73e8;
232
- "
233
- >
234
-
235
- </button>
236
- </div>
237
-
238
- <div id="controlsContent">
239
- <div class="date-inputs" style="display: flex; gap: 10px">
240
- <input
241
- type="number"
242
- id="tglInput"
243
- value="1"
244
- min="1"
245
- max="31"
246
- placeholder="Tanggal"
247
- />
248
- <input
249
- type="number"
250
- id="blnInput"
251
- value="1"
252
- min="1"
253
- max="12"
254
- placeholder="Bulan"
255
- />
256
- <input
257
- type="number"
258
- id="tahunInput"
259
- value="2024"
260
- min="2020"
261
- max="2030"
262
- placeholder="Tahun"
263
- />
264
- </div>
265
-
266
- <button onclick="runClustering()">
267
- Run Clustering in 4 Districts
268
- </button>
269
-
270
- <div id="notificationBox"></div>
271
- <table id="interpretasiKlaster"></table>
272
- </div>
273
- </div>
274
-
275
- <div id="chartBox">
276
- <div style="display: flex; justify-content: space-between">
277
- <h4 id="chartTitle"></h4>
278
- <button
279
- onclick="tutupChart()"
280
- style="
281
- border: none;
282
- background: none;
283
- font-size: 20px;
284
- cursor: pointer;
285
- "
286
- >
287
-
288
- </button>
289
- </div>
290
- <canvas id="forecastChart"></canvas>
291
- </div>
292
-
293
- <button
294
- onclick="toggleLegend(true)"
295
- style="
296
- position: absolute;
297
- bottom: 30px;
298
- left: 20px;
299
- z-index: 10001;
300
- padding: 5px 10px;
301
- background: #1a73e8;
302
- color: white;
303
- border: none;
304
- border-radius: 5px;
305
- cursor: pointer;
306
- "
307
- id="showLegendBtn"
308
- >
309
- ▶ Open Clustering
310
- </button>
311
-
312
- <div class="legend" id="clusterLegendPanel" style="display: none">
313
- <div
314
- style="
315
- display: flex;
316
- justify-content: space-between;
317
- align-items: center;
318
- "
319
- >
320
- <h4 id="legendTitle">Districts</h4>
321
- <button
322
- onclick="toggleLegend(false)"
323
- style="
324
- border: none;
325
- background: none;
326
- font-size: 20px;
327
- cursor: pointer;
328
- color: #333;
329
- "
330
- >
331
-
332
- </button>
333
- </div>
334
-
335
- <div id="legendContent"></div>
336
- </div>
337
- </div>
338
-
339
- <script>
340
- /* ====================== TOGGLE FUNCTIONS (SIDEBAR STYLE) ====================== */
341
-
342
- /**
343
- * Mengubah tampilan panel Clustering Controls (Sidebar)
344
- * @param {boolean} show - true untuk menampilkan panel, false untuk menyembunyikan.
345
- */
346
- function toggleControls(show) {
347
- const panel = document.getElementById("clusteringControlsPanel");
348
- const showBtn = document.getElementById("showControlsBtn");
349
-
350
- if (show) {
351
- panel.style.display = "block";
352
- showBtn.style.display = "none";
353
- } else {
354
- panel.style.display = "none";
355
- showBtn.style.display = "block";
356
- }
357
- }
358
-
359
- /**
360
- * Mengubah tampilan panel Legend (Sidebar Bawah)
361
- * @param {boolean} show - true untuk menampilkan panel, false untuk menyembunyikan.
362
- */
363
- function toggleLegend(show) {
364
- const panel = document.getElementById("clusterLegendPanel");
365
- const showBtn = document.getElementById("showLegendBtn");
366
-
367
- if (show) {
368
- panel.style.display = "block";
369
- showBtn.style.display = "none";
370
- } else {
371
- panel.style.display = "none";
372
- // Hanya tampilkan tombol buka legend jika map sudah dimuat
373
- if (geoJsonLayer) showBtn.style.display = "block";
374
- }
375
- }
376
-
377
- /* ====================== CHART ====================== */
378
- let chartInstance = null;
379
-
380
- function tutupChart() {
381
- document.getElementById("chartBox").style.display = "none";
382
- if (chartInstance) chartInstance.destroy();
383
- }
384
-
385
- function tampilkanGrafik(kabupaten, data) {
386
- document.getElementById("chartBox").style.display = "block";
387
- document.getElementById(
388
- "chartTitle"
389
- ).innerText = `Weather Forecasting ${kabupaten}`;
390
-
391
- const labels = [...data.last_days.dates, ...data.forecast.dates];
392
- const lastData = data.last_days.values.map((v) => v[0]);
393
- const forecastData = data.forecast.values.map((v) => v[0]);
394
-
395
- const ctx = document.getElementById("forecastChart");
396
-
397
- if (chartInstance) chartInstance.destroy();
398
-
399
- chartInstance = new Chart(ctx, {
400
- type: "line",
401
- data: {
402
- labels: labels,
403
- datasets: [
404
- {
405
- label: "Last 3 Days",
406
- data: lastData,
407
- borderColor: "#42a5f5",
408
- backgroundColor: "rgba(66, 165, 245, 0.5)",
409
- },
410
- {
411
- label: "5 Day Forecast",
412
- data: Array(lastData.length).fill(null).concat(forecastData),
413
- borderColor: "#ef5350",
414
- borderDash: [6, 4],
415
- backgroundColor: "transparent",
416
- },
417
- ],
418
- },
419
- options: {
420
- responsive: true,
421
- plugins: {
422
- legend: { position: "top" },
423
- title: { display: true, text: "Average Temperature(°C)" },
424
- },
425
- scales: {
426
- y: { beginAtZero: false },
427
- },
428
- },
429
- });
430
- }
431
-
432
- /* ====================== MAP ====================== */
433
- const clusterColors = { 0: "#5e35b1", 1: "#00897b", 2: "#fdd835" };
434
- const defaultWarnaKab = {
435
- Bangkalan: "#e41a1c",
436
- Sampang: "#377eb8",
437
- Pamekasan: "#4daf4a",
438
- Sumenep: "#984ea3",
439
- };
440
- const kabupatenList = ["Bangkalan", "Sampang", "Pamekasan", "Sumenep"];
441
-
442
- var map = L.map("map").setView([-7.0, 113.9], 10);
443
- var geoJsonLayer = null;
444
- var clusterResults = {};
445
-
446
- L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png").addTo(
447
- map
448
- );
449
-
450
- function styleFeature(feature) {
451
- const nama = feature.properties.nm_dati2;
452
- const clusterId = clusterResults[nama];
453
-
454
- if (clusterId !== undefined && clusterId !== null) {
455
- return {
456
- color: "#333",
457
- fillColor: clusterColors[clusterId] || "#ccc",
458
- fillOpacity: 0.8,
459
- weight: 2,
460
- };
461
- }
462
-
463
- return {
464
- color: "#444",
465
- fillColor: defaultWarnaKab[nama] || "#ccc",
466
- fillOpacity: 0.6,
467
- weight: 1,
468
- };
469
- }
470
-
471
- function updateLegend(mode) {
472
- const legendContent = document.getElementById("legendContent");
473
- const legendTitle = document.getElementById("legendTitle");
474
- legendContent.innerHTML = "";
475
-
476
- // Pastikan legend terbuka setelah clustering berhasil
477
- toggleLegend(true);
478
-
479
- if (mode === "kabupaten") {
480
- legendTitle.innerHTML = "Kabupaten";
481
- Object.keys(defaultWarnaKab).forEach((kab) => {
482
- legendContent.innerHTML += `
483
- <div class="item">
484
- <span class="swatch" style="background:${defaultWarnaKab[kab]}"></span>
485
- <div class="text-content">${kab}</div>
486
- </div>`;
487
- });
488
- } else {
489
- legendTitle.innerHTML = "Klaster Cuaca";
490
-
491
- const clusterDescriptions = {
492
- 0: {
493
- stats:
494
- "Rata-rata Suhu 33.2°C, Curah Hujan 100.0 mm, Kelembaban 78.7%",
495
- type: "Hari Hujan Lebat dan Panas",
496
- },
497
- 1: {
498
- stats:
499
- "Rata-rata Suhu 32.7°C, Curah Hujan 0.0 mm, Kelembaban 73.5%",
500
- type: "Hari Kering dan Panas",
501
- },
502
- 2: {
503
- stats:
504
- "Rata-rata Suhu 31.3°C, Curah Hujan 96.4 mm, Kelembaban 86.7%",
505
- type: "Hari Hujan Lebat dan Dingin/Sejuk",
506
- },
507
- };
508
- Object.keys(clusterColors).forEach((i) => {
509
- legendContent.innerHTML += `
510
- <div class="item">
511
- <span class="swatch" style="background:${clusterColors[i]}"></span>
512
- <div class="text-content">
513
- <b>Klaster ${i}</b> (${clusterDescriptions[i].type})<br>
514
- <small>${clusterDescriptions[i].stats}</small>
515
- </div>
516
- </div>
517
- `;
518
- });
519
- }
520
- }
521
-
522
- function loadKabupatenLayer() {
523
- if (geoJsonLayer) map.removeLayer(geoJsonLayer);
524
-
525
- clusterResults = {};
526
- updateLegend("kabupaten");
527
-
528
- // Sembunyikan Kontrol dan Legend saat memuat peta
529
- toggleControls(false);
530
- toggleLegend(false);
531
-
532
- fetch("{{ url_for('static', filename='Madura.geojson') }}")
533
- .then((res) => res.json())
534
- .then((data) => {
535
- geoJsonLayer = L.geoJSON(data, {
536
- style: styleFeature,
537
- onEachFeature: (feature, layer) => {
538
- const nama = feature.properties.nm_dati2;
539
-
540
- layer.bindPopup(`<b>${nama}</b>`);
541
-
542
- layer.on("click", () => {
543
- fetch(`/forecast/${nama}`)
544
- .then((res) => res.json())
545
- .then((data) => tampilkanGrafik(nama, data))
546
- .catch((err) =>
547
- alert(
548
- `Gagal mengambil data forecast untuk ${nama}: ${err.message}`
549
- )
550
- );
551
- });
552
- },
553
- }).addTo(map);
554
-
555
- map.fitBounds(geoJsonLayer.getBounds());
556
- toggleControls(true); // Tampilkan Kontrol Clustering secara default setelah loading
557
- toggleLegend(false); // Sembunyikan Legend Klaster setelah loading (akan muncul saat clustering)
558
- })
559
- .catch((err) => {
560
- console.error("Gagal memuat GeoJSON:", err);
561
- toggleControls(true); // Pastikan kontrol tetap bisa dibuka jika ada error
562
- });
563
- }
564
-
565
- function displayClusterInterpretation(clusterCenters, nFeatures) {
566
- const table = document.getElementById("interpretasiKlaster");
567
- table.innerHTML = "";
568
- table.style.display = "table";
569
-
570
- const featureNames = [
571
- "Suhu (°C)",
572
- "Curah Hujan (mm)",
573
- "Kelembaban (%)",
574
- ].slice(0, nFeatures);
575
- let headerHtml = "<tr><th>Klaster</th>";
576
- featureNames.forEach((name) => {
577
- headerHtml += `<th>${name}</th>`;
578
- });
579
- headerHtml += "</tr>";
580
- table.innerHTML += headerHtml;
581
-
582
- clusterCenters.forEach((center, index) => {
583
- let rowHtml = `<tr><td><b style="color:${
584
- clusterColors[index] || "#333"
585
- };">Klaster ${index}</b></td>`;
586
- center.forEach((value, i) => {
587
- let formattedValue;
588
- if (i === 0) formattedValue = `${value.toFixed(1)}°C`;
589
- else if (i === 1) formattedValue = `${value.toFixed(1)} mm`;
590
- else formattedValue = `${value.toFixed(1)}%`;
591
-
592
- rowHtml += `<td>${formattedValue}</td>`;
593
- });
594
- rowHtml += "</tr>";
595
- table.innerHTML += rowHtml;
596
- });
597
- }
598
-
599
- /* ====================== CLUSTERING ====================== */
600
- async function runClustering() {
601
- const tgl = document.getElementById("tglInput").value || "1";
602
- const bln = document.getElementById("blnInput").value || "1";
603
- const tahun = document.getElementById("tahunInput").value || "2024";
604
- const notificationBox = document.getElementById("notificationBox");
605
-
606
- notificationBox.innerHTML = `<span style="color:blue;"> Starting cluster prediction for 4 districts...</span>`;
607
-
608
- clusterResults = {};
609
- let allSuccess = true;
610
- let clusterCentersData = null;
611
-
612
- for (const kab of kabupatenList) {
613
- const url = `/clustering/${encodeURIComponent(
614
- kab
615
- )}?tgl=${encodeURIComponent(tgl)}&bln=${encodeURIComponent(
616
- bln
617
- )}&tahun=${encodeURIComponent(tahun)}`;
618
-
619
- try {
620
- const res = await fetch(url);
621
- const text = await res.text();
622
-
623
- try {
624
- const data = JSON.parse(text);
625
- if (!res.ok) {
626
- const msg = data.message || data.error || text;
627
- console.error(`Clustering ${kab} gagal:`, msg);
628
- notificationBox.innerHTML += `<br><span style="color:red;"> Fail ${kab}: ${msg}</span>`;
629
- allSuccess = false;
630
- continue;
631
- }
632
-
633
- const predictedCluster =
634
- data.predicted_cluster ??
635
- data.predicted ??
636
- data.cluster ??
637
- data.predictedCluster;
638
- if (predictedCluster === undefined || predictedCluster === null) {
639
- console.error(
640
- `Respon ${kab} tidak valid: tidak ada field klaster.`,
641
- data
642
- );
643
- notificationBox.innerHTML += `<br><span style="color:red;"> Respon ${kab} tidak valid (tanpa klaster).</span>`;
644
- allSuccess = false;
645
- continue;
646
- }
647
-
648
- const clusterId = Number(predictedCluster);
649
- clusterResults[kab] = clusterId;
650
-
651
- if (Array.isArray(data.cluster_centers) && !clusterCentersData) {
652
- clusterCentersData = data.cluster_centers;
653
- }
654
- } catch (err) {
655
- console.error(
656
- `Gagal parsing respon JSON dari ${kab}:`,
657
- text,
658
- err
659
- );
660
- notificationBox.innerHTML += `<br><span style="color:red;"> Fail parsing respon ${kab}.</span>`;
661
- allSuccess = false;
662
- }
663
- } catch (err) {
664
- notificationBox.innerHTML += `<br><span style="color:red;"> Fail fetch API ${kab}: ${err.message}. Pastikan server Flask berjalan.</span>`;
665
- console.error(`Clustering fetch error ${kab}:`, err);
666
- allSuccess = false;
667
- }
668
- }
669
-
670
- if (geoJsonLayer) {
671
- geoJsonLayer.setStyle(styleFeature);
672
- } else {
673
- loadKabupatenLayer();
674
- }
675
-
676
- // Panggil updateLegend untuk menampilkan hasil klaster (ini juga memanggil toggleLegend(true))
677
- updateLegend("cluster");
678
- }
679
-
680
- // Muat peta saat halaman pertama dimuat
681
- loadKabupatenLayer();
682
- </script>
683
- </body>
684
- </html>
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Madura weather</title>
5
+ <meta charset="utf-8" />
6
+ <link
7
+ rel="stylesheet"
8
+ href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
9
+ />
10
+
11
+ <style>
12
+ body {
13
+ font-family: Arial, sans-serif;
14
+ margin: 0;
15
+ padding: 0;
16
+ min-height: 100vh;
17
+ background-color: #f4f4f4;
18
+ display: flex; /* Menggunakan flexbox untuk konten utama */
19
+ flex-direction: column;
20
+ }
21
+ html,
22
+ body,
23
+ #map {
24
+ height: 100%;
25
+ margin: 0;
26
+ padding: 0;
27
+ }
28
+
29
+ /* ================= LEGEND ================= */
30
+ /* Mengubah posisi z-index untuk mencegah tumpang tindih */
31
+ .legend {
32
+ position: absolute;
33
+ bottom: 30px;
34
+ left: 20px;
35
+ background: white;
36
+ padding: 10px;
37
+ line-height: 1.5;
38
+ border-radius: 6px;
39
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
40
+ font-size: 13px;
41
+ z-index: 9999;
42
+ }
43
+ .legend h4 {
44
+ margin: 0 0 5px 0;
45
+ font-size: 14px;
46
+ font-weight: bold;
47
+ }
48
+ .legend .item {
49
+ display: flex;
50
+ align-items: flex-start;
51
+ gap: 6px;
52
+ margin-bottom: 5px;
53
+ }
54
+ .legend .swatch {
55
+ width: 18px;
56
+ height: 12px;
57
+ border: 1px solid #444;
58
+ flex-shrink: 0;
59
+ margin-top: 3px;
60
+ }
61
+ .legend .text-content {
62
+ display: flex;
63
+ flex-direction: column;
64
+ text-align: left;
65
+ }
66
+
67
+ /* ================= NAVBAR ================= */
68
+ .navbar {
69
+ display: flex;
70
+ justify-content: space-between;
71
+ align-items: center;
72
+ background: #1a73e8;
73
+ padding: 10px 20px;
74
+ color: white;
75
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
76
+ position: relative;
77
+ z-index: 10000;
78
+ }
79
+ .navbar .brand {
80
+ font-size: 18px;
81
+ font-weight: bold;
82
+ }
83
+ .navbar .menu {
84
+ list-style: none;
85
+ display: flex;
86
+ gap: 20px;
87
+ margin: 0;
88
+ padding: 0;
89
+ }
90
+ .navbar .menu li a {
91
+ color: white;
92
+ text-decoration: none;
93
+ }
94
+
95
+ /* ================= CLUSTER CONTROL ================= */
96
+ .clustering-controls {
97
+ position: absolute;
98
+ top: 80px;
99
+ left: 20px;
100
+ width: 300px;
101
+ background: white;
102
+ border-radius: 10px;
103
+ padding: 15px;
104
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
105
+ z-index: 9999;
106
+ }
107
+ .clustering-controls h3 {
108
+ color: #1a73e8;
109
+ margin-top: 0;
110
+ border-bottom: 1px solid #eee;
111
+ padding-bottom: 8px;
112
+ }
113
+ .clustering-controls select,
114
+ .clustering-controls input[type="number"] {
115
+ width: 100%;
116
+ padding: 8px;
117
+ margin-bottom: 10px;
118
+ border: 1px solid #ccc;
119
+ }
120
+ .clustering-controls button {
121
+ width: 100%;
122
+ padding: 10px;
123
+ background: #4caf50;
124
+ border: none;
125
+ border-radius: 5px;
126
+ color: white;
127
+ font-weight: bold;
128
+ cursor: pointer;
129
+ }
130
+ .clustering-controls button:hover {
131
+ background: #45a049;
132
+ }
133
+
134
+ #notificationBox {
135
+ margin-top: 15px;
136
+ padding: 10px;
137
+ border-radius: 4px;
138
+ font-size: 12px;
139
+ }
140
+
141
+ /* ================= TABEL INTERPRETASI ================= */
142
+ #interpretasiKlaster {
143
+ width: 100%;
144
+ margin-top: 15px;
145
+ border-collapse: collapse;
146
+ font-size: 11px;
147
+ display: none;
148
+ }
149
+ #interpretasiKlaster th,
150
+ #interpretasiKlaster td {
151
+ border: 1px solid #ddd;
152
+ padding: 6px;
153
+ text-align: center;
154
+ }
155
+ #interpretasiKlaster th {
156
+ background: #f2f2f2;
157
+ }
158
+
159
+ /* ================= CHART BOX ================= */
160
+ #chartBox {
161
+ position: absolute;
162
+ top: 80px;
163
+ right: 20px;
164
+ width: 450px;
165
+ background: white;
166
+ border-radius: 10px;
167
+ padding: 15px;
168
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
169
+ z-index: 9999;
170
+ display: none;
171
+ }
172
+ </style>
173
+
174
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
175
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
176
+ </head>
177
+
178
+ <body>
179
+ <header>
180
+ <nav class="navbar">
181
+ <div class="brand">Madura Weather</div>
182
+ <ul class="menu">
183
+ <li><a href="/">Homepage</a></li>
184
+ <li><a href="/crop">Recomendation Plant</a></li>
185
+ <li><a href="/cuaca">Image Prediction</a></li>
186
+ <li><a href="/about">About</a></li>
187
+ </ul>
188
+ </nav>
189
+ </header>
190
+
191
+ <div id="map">
192
+ <button
193
+ onclick="toggleControls(true)"
194
+ style="
195
+ position: absolute;
196
+ top: 80px;
197
+ left: 20px;
198
+ z-index: 10001;
199
+ padding: 5px 10px;
200
+ background: #1a73e8;
201
+ color: white;
202
+ border: none;
203
+ border-radius: 5px;
204
+ cursor: pointer;
205
+ "
206
+ id="showControlsBtn"
207
+ >
208
+ ▶ Control
209
+ </button>
210
+
211
+ <div
212
+ class="clustering-controls"
213
+ id="clusteringControlsPanel"
214
+ style="display: none"
215
+ >
216
+ <div
217
+ style="
218
+ display: flex;
219
+ justify-content: space-between;
220
+ align-items: center;
221
+ "
222
+ >
223
+ <h3>Clustering Simulation (K-Means)</h3>
224
+ <button
225
+ onclick="toggleControls(false)"
226
+ style="
227
+ border: none;
228
+ background: none;
229
+ font-size: 20px;
230
+ cursor: pointer;
231
+ color: #1a73e8;
232
+ "
233
+ >
234
+
235
+ </button>
236
+ </div>
237
+
238
+ <div id="controlsContent">
239
+ <div class="date-inputs" style="display: flex; gap: 10px">
240
+ <input
241
+ type="number"
242
+ id="tglInput"
243
+ value="1"
244
+ min="1"
245
+ max="31"
246
+ placeholder="Tanggal"
247
+ />
248
+ <input
249
+ type="number"
250
+ id="blnInput"
251
+ value="1"
252
+ min="1"
253
+ max="12"
254
+ placeholder="Bulan"
255
+ />
256
+ <input
257
+ type="number"
258
+ id="tahunInput"
259
+ value="2024"
260
+ min="2020"
261
+ max="2030"
262
+ placeholder="Tahun"
263
+ />
264
+ </div>
265
+
266
+ <button onclick="runClustering()">
267
+ Run Clustering in 4 Districts
268
+ </button>
269
+
270
+ <div id="notificationBox"></div>
271
+ <table id="interpretasiKlaster"></table>
272
+ </div>
273
+ </div>
274
+
275
+ <div id="chartBox">
276
+ <div style="display: flex; justify-content: space-between">
277
+ <h4 id="chartTitle"></h4>
278
+ <button
279
+ onclick="tutupChart()"
280
+ style="
281
+ border: none;
282
+ background: none;
283
+ font-size: 20px;
284
+ cursor: pointer;
285
+ "
286
+ >
287
+
288
+ </button>
289
+ </div>
290
+ <canvas id="forecastChart"></canvas>
291
+ </div>
292
+
293
+ <button
294
+ onclick="toggleLegend(true)"
295
+ style="
296
+ position: absolute;
297
+ bottom: 30px;
298
+ left: 20px;
299
+ z-index: 10001;
300
+ padding: 5px 10px;
301
+ background: #1a73e8;
302
+ color: white;
303
+ border: none;
304
+ border-radius: 5px;
305
+ cursor: pointer;
306
+ "
307
+ id="showLegendBtn"
308
+ >
309
+ ▶ Open Clustering
310
+ </button>
311
+
312
+ <div class="legend" id="clusterLegendPanel" style="display: none">
313
+ <div
314
+ style="
315
+ display: flex;
316
+ justify-content: space-between;
317
+ align-items: center;
318
+ "
319
+ >
320
+ <h4 id="legendTitle">Districts</h4>
321
+ <button
322
+ onclick="toggleLegend(false)"
323
+ style="
324
+ border: none;
325
+ background: none;
326
+ font-size: 20px;
327
+ cursor: pointer;
328
+ color: #333;
329
+ "
330
+ >
331
+
332
+ </button>
333
+ </div>
334
+
335
+ <div id="legendContent"></div>
336
+ </div>
337
+ </div>
338
+
339
+ <script>
340
+ /* ====================== TOGGLE FUNCTIONS (SIDEBAR STYLE) ====================== */
341
+
342
+ /**
343
+ * Mengubah tampilan panel Clustering Controls (Sidebar)
344
+ * @param {boolean} show - true untuk menampilkan panel, false untuk menyembunyikan.
345
+ */
346
+ function toggleControls(show) {
347
+ const panel = document.getElementById("clusteringControlsPanel");
348
+ const showBtn = document.getElementById("showControlsBtn");
349
+
350
+ if (show) {
351
+ panel.style.display = "block";
352
+ showBtn.style.display = "none";
353
+ } else {
354
+ panel.style.display = "none";
355
+ showBtn.style.display = "block";
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Mengubah tampilan panel Legend (Sidebar Bawah)
361
+ * @param {boolean} show - true untuk menampilkan panel, false untuk menyembunyikan.
362
+ */
363
+ function toggleLegend(show) {
364
+ const panel = document.getElementById("clusterLegendPanel");
365
+ const showBtn = document.getElementById("showLegendBtn");
366
+
367
+ if (show) {
368
+ panel.style.display = "block";
369
+ showBtn.style.display = "none";
370
+ } else {
371
+ panel.style.display = "none";
372
+ // Hanya tampilkan tombol buka legend jika map sudah dimuat
373
+ if (geoJsonLayer) showBtn.style.display = "block";
374
+ }
375
+ }
376
+
377
+ /* ====================== CHART ====================== */
378
+ let chartInstance = null;
379
+
380
+ function tutupChart() {
381
+ document.getElementById("chartBox").style.display = "none";
382
+ if (chartInstance) chartInstance.destroy();
383
+ }
384
+
385
+ function tampilkanGrafik(kabupaten, data) {
386
+ document.getElementById("chartBox").style.display = "block";
387
+ document.getElementById(
388
+ "chartTitle"
389
+ ).innerText = `Weather Forecasting ${kabupaten}`;
390
+
391
+ const labels = [...data.last_days.dates, ...data.forecast.dates];
392
+ const lastData = data.last_days.values.map((v) => v[0]);
393
+ const forecastData = data.forecast.values.map((v) => v[0]);
394
+
395
+ const ctx = document.getElementById("forecastChart");
396
+
397
+ if (chartInstance) chartInstance.destroy();
398
+
399
+ chartInstance = new Chart(ctx, {
400
+ type: "line",
401
+ data: {
402
+ labels: labels,
403
+ datasets: [
404
+ {
405
+ label: "Last 3 Days",
406
+ data: lastData,
407
+ borderColor: "#42a5f5",
408
+ backgroundColor: "rgba(66, 165, 245, 0.5)",
409
+ },
410
+ {
411
+ label: "5 Day Forecast",
412
+ data: Array(lastData.length).fill(null).concat(forecastData),
413
+ borderColor: "#ef5350",
414
+ borderDash: [6, 4],
415
+ backgroundColor: "transparent",
416
+ },
417
+ ],
418
+ },
419
+ options: {
420
+ responsive: true,
421
+ plugins: {
422
+ legend: { position: "top" },
423
+ title: { display: true, text: "Average Temperature(°C)" },
424
+ },
425
+ scales: {
426
+ y: { beginAtZero: false },
427
+ },
428
+ },
429
+ });
430
+ }
431
+
432
+ /* ====================== MAP ====================== */
433
+ const clusterColors = { 0: "#5e35b1", 1: "#00897b", 2: "#fdd835" };
434
+ const defaultWarnaKab = {
435
+ Bangkalan: "#e41a1c",
436
+ Sampang: "#377eb8",
437
+ Pamekasan: "#4daf4a",
438
+ Sumenep: "#984ea3",
439
+ };
440
+ const kabupatenList = ["Bangkalan", "Sampang", "Pamekasan", "Sumenep"];
441
+
442
+ var map = L.map("map").setView([-7.0, 113.9], 10);
443
+ var geoJsonLayer = null;
444
+ var clusterResults = {};
445
+
446
+ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png").addTo(
447
+ map
448
+ );
449
+
450
+ function styleFeature(feature) {
451
+ const nama = feature.properties.nm_dati2;
452
+ const clusterId = clusterResults[nama];
453
+
454
+ if (clusterId !== undefined && clusterId !== null) {
455
+ return {
456
+ color: "#333",
457
+ fillColor: clusterColors[clusterId] || "#ccc",
458
+ fillOpacity: 0.8,
459
+ weight: 2,
460
+ };
461
+ }
462
+
463
+ return {
464
+ color: "#444",
465
+ fillColor: defaultWarnaKab[nama] || "#ccc",
466
+ fillOpacity: 0.6,
467
+ weight: 1,
468
+ };
469
+ }
470
+
471
+ function updateLegend(mode) {
472
+ const legendContent = document.getElementById("legendContent");
473
+ const legendTitle = document.getElementById("legendTitle");
474
+ legendContent.innerHTML = "";
475
+
476
+ // Pastikan legend terbuka setelah clustering berhasil
477
+ toggleLegend(true);
478
+
479
+ if (mode === "kabupaten") {
480
+ legendTitle.innerHTML = "Kabupaten";
481
+ Object.keys(defaultWarnaKab).forEach((kab) => {
482
+ legendContent.innerHTML += `
483
+ <div class="item">
484
+ <span class="swatch" style="background:${defaultWarnaKab[kab]}"></span>
485
+ <div class="text-content">${kab}</div>
486
+ </div>`;
487
+ });
488
+ } else {
489
+ legendTitle.innerHTML = "Klaster Cuaca";
490
+
491
+ const clusterDescriptions = {
492
+ 0: {
493
+ stats:
494
+ "Rata-rata Suhu 33.2°C, Curah Hujan 100.0 mm, Kelembaban 78.7%",
495
+ type: "Hari Hujan Lebat dan Panas",
496
+ },
497
+ 1: {
498
+ stats:
499
+ "Rata-rata Suhu 32.7°C, Curah Hujan 0.0 mm, Kelembaban 73.5%",
500
+ type: "Hari Kering dan Panas",
501
+ },
502
+ 2: {
503
+ stats:
504
+ "Rata-rata Suhu 31.3°C, Curah Hujan 96.4 mm, Kelembaban 86.7%",
505
+ type: "Hari Hujan Lebat dan Dingin/Sejuk",
506
+ },
507
+ };
508
+ Object.keys(clusterColors).forEach((i) => {
509
+ legendContent.innerHTML += `
510
+ <div class="item">
511
+ <span class="swatch" style="background:${clusterColors[i]}"></span>
512
+ <div class="text-content">
513
+ <b>Klaster ${i}</b> (${clusterDescriptions[i].type})<br>
514
+ <small>${clusterDescriptions[i].stats}</small>
515
+ </div>
516
+ </div>
517
+ `;
518
+ });
519
+ }
520
+ }
521
+
522
+ function loadKabupatenLayer() {
523
+ if (geoJsonLayer) map.removeLayer(geoJsonLayer);
524
+
525
+ clusterResults = {};
526
+ updateLegend("kabupaten");
527
+
528
+ // Sembunyikan Kontrol dan Legend saat memuat peta
529
+ toggleControls(false);
530
+ toggleLegend(false);
531
+
532
+ fetch("{{ url_for('static', filename='Madura.geojson') }}")
533
+ .then((res) => res.json())
534
+ .then((data) => {
535
+ geoJsonLayer = L.geoJSON(data, {
536
+ style: styleFeature,
537
+ onEachFeature: (feature, layer) => {
538
+ const nama = feature.properties.nm_dati2;
539
+
540
+ layer.bindPopup(`<b>${nama}</b>`);
541
+
542
+ layer.on("click", () => {
543
+ fetch(`/forecast/${nama}`)
544
+ .then((res) => res.json())
545
+ .then((data) => tampilkanGrafik(nama, data))
546
+ .catch((err) =>
547
+ alert(
548
+ `Gagal mengambil data forecast untuk ${nama}: ${data}`
549
+ )
550
+ );
551
+ });
552
+ },
553
+ }).addTo(map);
554
+
555
+ map.fitBounds(geoJsonLayer.getBounds());
556
+ toggleControls(true); // Tampilkan Kontrol Clustering secara default setelah loading
557
+ toggleLegend(false); // Sembunyikan Legend Klaster setelah loading (akan muncul saat clustering)
558
+ })
559
+ .catch((err) => {
560
+ console.error("Gagal memuat GeoJSON:", err);
561
+ toggleControls(true); // Pastikan kontrol tetap bisa dibuka jika ada error
562
+ });
563
+ }
564
+
565
+ function displayClusterInterpretation(clusterCenters, nFeatures) {
566
+ const table = document.getElementById("interpretasiKlaster");
567
+ table.innerHTML = "";
568
+ table.style.display = "table";
569
+
570
+ const featureNames = [
571
+ "Suhu (°C)",
572
+ "Curah Hujan (mm)",
573
+ "Kelembaban (%)",
574
+ ].slice(0, nFeatures);
575
+ let headerHtml = "<tr><th>Klaster</th>";
576
+ featureNames.forEach((name) => {
577
+ headerHtml += `<th>${name}</th>`;
578
+ });
579
+ headerHtml += "</tr>";
580
+ table.innerHTML += headerHtml;
581
+
582
+ clusterCenters.forEach((center, index) => {
583
+ let rowHtml = `<tr><td><b style="color:${
584
+ clusterColors[index] || "#333"
585
+ };">Klaster ${index}</b></td>`;
586
+ center.forEach((value, i) => {
587
+ let formattedValue;
588
+ if (i === 0) formattedValue = `${value.toFixed(1)}°C`;
589
+ else if (i === 1) formattedValue = `${value.toFixed(1)} mm`;
590
+ else formattedValue = `${value.toFixed(1)}%`;
591
+
592
+ rowHtml += `<td>${formattedValue}</td>`;
593
+ });
594
+ rowHtml += "</tr>";
595
+ table.innerHTML += rowHtml;
596
+ });
597
+ }
598
+
599
+ /* ====================== CLUSTERING ====================== */
600
+ async function runClustering() {
601
+ const tgl = document.getElementById("tglInput").value || "1";
602
+ const bln = document.getElementById("blnInput").value || "1";
603
+ const tahun = document.getElementById("tahunInput").value || "2024";
604
+ const notificationBox = document.getElementById("notificationBox");
605
+
606
+ notificationBox.innerHTML = `<span style="color:blue;"> Starting cluster prediction for 4 districts...</span>`;
607
+
608
+ clusterResults = {};
609
+ let allSuccess = true;
610
+ let clusterCentersData = null;
611
+
612
+ for (const kab of kabupatenList) {
613
+ const url = `/clustering/${encodeURIComponent(
614
+ kab
615
+ )}?tgl=${encodeURIComponent(tgl)}&bln=${encodeURIComponent(
616
+ bln
617
+ )}&tahun=${encodeURIComponent(tahun)}`;
618
+
619
+ try {
620
+ const res = await fetch(url);
621
+ const text = await res.text();
622
+
623
+ try {
624
+ const data = JSON.parse(text);
625
+ if (!res.ok) {
626
+ const msg = data.message || data.error || text;
627
+ console.error(`Clustering ${kab} gagal:`, msg);
628
+ notificationBox.innerHTML += `<br><span style="color:red;"> Fail ${kab}: ${msg}</span>`;
629
+ allSuccess = false;
630
+ continue;
631
+ }
632
+
633
+ const predictedCluster =
634
+ data.predicted_cluster ??
635
+ data.predicted ??
636
+ data.cluster ??
637
+ data.predictedCluster;
638
+ if (predictedCluster === undefined || predictedCluster === null) {
639
+ console.error(
640
+ `Respon ${kab} tidak valid: tidak ada field klaster.`,
641
+ data
642
+ );
643
+ notificationBox.innerHTML += `<br><span style="color:red;"> Respon ${kab} tidak valid (tanpa klaster).</span>`;
644
+ allSuccess = false;
645
+ continue;
646
+ }
647
+
648
+ const clusterId = Number(predictedCluster);
649
+ clusterResults[kab] = clusterId;
650
+
651
+ if (Array.isArray(data.cluster_centers) && !clusterCentersData) {
652
+ clusterCentersData = data.cluster_centers;
653
+ }
654
+ } catch (err) {
655
+ console.error(
656
+ `Gagal parsing respon JSON dari ${kab}:`,
657
+ text,
658
+ err
659
+ );
660
+ notificationBox.innerHTML += `<br><span style="color:red;"> Fail parsing respon ${kab}.</span>`;
661
+ allSuccess = false;
662
+ }
663
+ } catch (err) {
664
+ notificationBox.innerHTML += `<br><span style="color:red;"> Fail fetch API ${kab}: ${err.message}. Pastikan server Flask berjalan.</span>`;
665
+ console.error(`Clustering fetch error ${kab}:`, err);
666
+ allSuccess = false;
667
+ }
668
+ }
669
+
670
+ if (geoJsonLayer) {
671
+ geoJsonLayer.setStyle(styleFeature);
672
+ } else {
673
+ loadKabupatenLayer();
674
+ }
675
+
676
+ // Panggil updateLegend untuk menampilkan hasil klaster (ini juga memanggil toggleLegend(true))
677
+ updateLegend("cluster");
678
+ }
679
+
680
+ // Muat peta saat halaman pertama dimuat
681
+ loadKabupatenLayer();
682
+ </script>
683
+ </body>
684
+ </html>