atsuga commited on
Commit
c30e73e
·
verified ·
1 Parent(s): 381515e

Upload 23 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ static/user.png filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import joblib
2
+ import pandas as pd
3
+ from flask import Flask, request, jsonify, render_template
4
+ from datetime import timedelta
5
+ import os
6
+
7
+ from tensorflow.keras.models import load_model
8
+ from tensorflow.keras.preprocessing.image import load_img, img_to_array
9
+ import numpy as np
10
+
11
+ # ==============================================================================
12
+ # --- 1. KONFIGURASI JALUR & VARIABEL GLOBAL ---
13
+ # ==============================================================================
14
+
15
+ # --- A. Jalur Model Prediction Crop (Teman Anda) ---
16
+ CROP_PRED_MODEL_PATH = 'random_forest_model.pkl'
17
+ CROP_PRED_SCALER_PATH = 'scaler.pkl'
18
+
19
+ # --- B. Jalur Model Clustering (Anda) ---
20
+ MODEL_FILES_CLUSTERING = {
21
+ "bangkalan": "kmeans_model_bangkalan.joblib",
22
+ "sampang" : "kmeans_model_sampang.joblib",
23
+ "pamekasan": "kmeans_model_pamekasan.joblib",
24
+ "sumenep": "kmeans_model_sumenep.joblib",
25
+ # Tambahkan kabupaten lain di sini
26
+ }
27
+ SCALER_FILES = {
28
+ "bangkalan": "scaler_bangkalan.joblib",
29
+ "sampang": "scaler_sampang.joblib",
30
+ "pamekasan": "scaler_pamekasan.joblib",
31
+ "sumenep": "scaler_sumenep.joblib",
32
+ # Tambahkan kabupaten lain di sini
33
+ }
34
+
35
+ # --- C. Jalur Model VAR (Forecasting - Kritis) ---
36
+ # Menggunakan variabel dari kode teman Anda, namun diubah agar path tidak hardcode Windows
37
+ VAR_MODEL_PATH = "var_model_multivariate.joblib"
38
+
39
+ # --- D. Mapping Data CSV ---
40
+ # Ganti dengan path CSV yang AKTUAL di server Anda
41
+ CSV_MAP = {
42
+ "Bangkalan": "static/bangkalan.csv",
43
+ "Sampang": "static/sampang.csv",
44
+ "Pamekasan": "static/pamekasan.csv",
45
+ "Sumenep": "static/sumenep.csv",
46
+ }
47
+
48
+ # --- E. Variabel Global untuk Model yang Dimuat (Termasuk Model Teman Anda) ---
49
+ LOADED_MODELS_CLUSTERING = {}
50
+ LOADED_SCALERS = {}
51
+ VAR_MODEL = None # Variabel global model VAR
52
+ CROP_MODEL = None # Model Random Forest untuk crop prediction
53
+ CROP_SCALER = None # Scaler Crop Prediction
54
+
55
+ SUPPORTED_KABUPATEN_CLUSTERING = list(MODEL_FILES_CLUSTERING.keys())
56
+ FEATURE_NAMES_CROP = ['N', 'P', 'K', 'temperature', 'humidity', 'ph', 'rainfall']
57
+
58
+ # Model CUACA (Image Classification)
59
+ WEATHER_MODEL = None
60
+ WEATHER_CLASSES = [
61
+ "dew", "fogsmog", "frost", "glaze", "hail",
62
+ "lightning", "rain", "rainbow", "rime",
63
+ "sandstorm", "snow"
64
+ ]
65
+
66
+ # ==============================================================================
67
+ # --- 2. FUNGSI LOADING SEMUA ASET (Berjalan saat Startup) ---
68
+ # ==============================================================================
69
+
70
+ def load_all_assets():
71
+ """Memuat semua model (Clustering, Forecasting, Crop Prediction) ke memori."""
72
+ global LOADED_MODELS_CLUSTERING, LOADED_SCALERS, VAR_MODEL, CROP_MODEL, CROP_SCALER
73
+ print("--- MEMUAT SEMUA ASET ---")
74
+
75
+ # A. Muat Model Crop Prediction (Teman Anda)
76
+ try:
77
+ CROP_MODEL = joblib.load(CROP_PRED_MODEL_PATH)
78
+ CROP_SCALER = joblib.load(CROP_PRED_SCALER_PATH)
79
+ print(f"Model CROP ({CROP_PRED_MODEL_PATH}) dan Scaler berhasil dimuat.")
80
+ except Exception as e:
81
+ print(f"GAGAL memuat model CROP: {e}")
82
+
83
+ # B. Muat Model Clustering (Anda)
84
+ for kab, filename in MODEL_FILES_CLUSTERING.items():
85
+ try:
86
+ model_loaded = joblib.load(filename)
87
+ LOADED_MODELS_CLUSTERING[kab] = model_loaded
88
+ print(f"Model CLUSTERING {kab.title()} ({filename}) berhasil dimuat.")
89
+ except Exception as e:
90
+ print(f"GAGAL memuat model CLUSTERING {kab.title()} ({filename}): {e}")
91
+ LOADED_MODELS_CLUSTERING[kab] = None
92
+
93
+ # C. Muat Scaler Clustering (Anda)
94
+ for kab, filename in SCALER_FILES.items():
95
+ try:
96
+ scaler_loaded = joblib.load(filename)
97
+ LOADED_SCALERS[kab] = scaler_loaded
98
+ print(f"Scaler CLUSTERING {kab.title()} ({filename}) berhasil dimuat.")
99
+ except Exception as e:
100
+ print(f"GAGAL memuat scaler CLUSTERING {kab.title()} ({filename}): {e}")
101
+ LOADED_SCALERS[kab] = None
102
+
103
+ # D. Muat Model VAR/Forecasting
104
+ try:
105
+ VAR_MODEL = joblib.load(VAR_MODEL_PATH)
106
+ print(f"Model VAR ({VAR_MODEL_PATH}) berhasil dimuat.")
107
+ except Exception as e:
108
+ print(f"GAGAL memuat model VAR: {e}. PASTIKAN PATH BENAR.")
109
+ VAR_MODEL = None
110
+
111
+ # E. Muat Model Cuaca (Image Classification)
112
+ global WEATHER_MODEL
113
+ try:
114
+ WEATHER_MODEL = load_model("model_cuaca.h5")
115
+ print("Model CUACA berhasil dimuat.")
116
+ except Exception as e:
117
+ WEATHER_MODEL = None
118
+ print(f"GAGAL memuat model CUACA: {e}")
119
+
120
+
121
+
122
+ print("--- SELESAI MEMUAT SEMUA ASET ---")
123
+
124
+ # Panggil fungsi ini saat startup!
125
+ load_all_assets()
126
+
127
+ # ==============================================================================
128
+ # --- 3. INISIALISASI FLASK & ROUTE CROP PREDICTION (Teman Anda) ---
129
+ # ==============================================================================
130
+
131
+ app = Flask(__name__)
132
+
133
+ @app.route('/')
134
+ def home():
135
+ # Asumsi Anda memiliki template index.html
136
+ return render_template('index.html')
137
+
138
+ @app.route('/crop')
139
+ def crop():
140
+ # Asumsi Anda memiliki template Crop.html
141
+ return render_template('Crop.html', feature_names=FEATURE_NAMES_CROP, form_values={})
142
+
143
+ @app.route('/predict', methods=['POST'])
144
+ def predict():
145
+ # Menggunakan CROP_MODEL dan CROP_SCALER global yang sudah dimuat
146
+ if CROP_MODEL is None or CROP_SCALER is None:
147
+ return "Error: Model atau Scaler Crop Prediction tidak dimuat. Cek file .pkl Anda.", 500
148
+
149
+ try:
150
+ data = request.form.to_dict()
151
+ input_features = []
152
+ form_values = {}
153
+
154
+ for name in FEATURE_NAMES_CROP:
155
+ value = float(data[name])
156
+ input_features.append(value)
157
+ form_values[name] = value
158
+
159
+ input_df = pd.DataFrame([input_features], columns=FEATURE_NAMES_CROP)
160
+
161
+ # Scaling Data Input
162
+ features_scaled = CROP_SCALER.transform(input_df)
163
+ features_scaled = pd.DataFrame(features_scaled, columns=input_df.columns)
164
+
165
+ prediction = CROP_MODEL.predict(features_scaled)
166
+
167
+ translate = {
168
+ 'rice': 'padi',
169
+ 'maize': 'jagung',
170
+ 'jute': 'rami',
171
+ 'cotton': 'kapas',
172
+ 'coconut': 'kelapa',
173
+ 'papaya': 'pepaya',
174
+ 'orange': 'jeruk',
175
+ 'apple': 'apel',
176
+ 'muskmelon': 'blewah',
177
+ 'watermelon': 'semangka',
178
+ 'grapes': 'anggur',
179
+ 'mango': 'mangga',
180
+ 'banana': 'pisang',
181
+ 'pomegranate': 'delima',
182
+ 'lentil': 'lentil',
183
+ 'blackgram': 'kacang tunggak',
184
+ 'mungbean': 'kacang hijau',
185
+ 'mothbeans': 'kacang ngengat',
186
+ 'pigeonpeas': 'kacang gude',
187
+ 'kidneybeans': 'kacang merah',
188
+ 'chickpea': 'kacang arab',
189
+ 'coffee': 'kopi'
190
+ }
191
+
192
+
193
+ output = translate[prediction[0]]
194
+
195
+
196
+ # Asumsi Anda memiliki template Crop.html
197
+ return render_template('Crop.html',
198
+ prediction_text=f'{output}',
199
+ feature_names=FEATURE_NAMES_CROP,
200
+ form_values=form_values)
201
+
202
+ except KeyError as e:
203
+ # Asumsi Anda memiliki template Crop.html
204
+ return render_template('Crop.html',
205
+ error_message=f'Error: Input untuk fitur {str(e)} hilang. Pastikan semua kolom terisi.',
206
+ feature_names=FEATURE_NAMES_CROP,
207
+ form_values=request.form.to_dict()), 400
208
+ except ValueError:
209
+ # Asumsi Anda memiliki template Crop.html
210
+ return render_template('Crop.html',
211
+ error_message='Error: Semua input harus berupa angka.',
212
+ feature_names=FEATURE_NAMES_CROP,
213
+ form_values=request.form.to_dict()), 400
214
+ except Exception as e:
215
+ # Asumsi Anda memiliki template Crop.html
216
+ return render_template('Crop.html',
217
+ error_message=f'Terjadi error tak terduga: {str(e)}',
218
+ feature_names=FEATURE_NAMES_CROP,
219
+ form_values=request.form.to_dict()), 500
220
+
221
+ # ==============================================================================
222
+ # --- 4. ROUTE FORECASTING PER KABUPATEN (Perbaikan Kode Teman Anda) ---
223
+ # ==============================================================================
224
+
225
+
226
+ @app.route('/forecast/<kabupaten>')
227
+ def forecast(kabupaten):
228
+ try:
229
+ csv_map = {
230
+ "Bangkalan": r"static\bangkalan.csv",
231
+ "Sampang": r"static\sampang.csv",
232
+ "Pamekasan": r"static\pamekasan.csv",
233
+ "Sumenep": r"static\sumenep.csv"
234
+ }
235
+
236
+ model_path = r"static\var_model_multivariate.joblib"
237
+
238
+ if kabupaten not in csv_map:
239
+ return jsonify({"error": "Kabupaten tidak ditemukan"}), 404
240
+
241
+ # =====================
242
+ # BACA CSV
243
+ # =====================
244
+ df = pd.read_csv(csv_map[kabupaten])
245
+
246
+ # =====================
247
+ # SET INDEX WAKTU
248
+ # =====================
249
+ df['datetime'] = pd.to_datetime(df['datetime'])
250
+ df.set_index('datetime', inplace=True)
251
+
252
+ # =====================
253
+ # LOAD MODEL VAR
254
+ # =====================
255
+ var_model = joblib.load(model_path)
256
+
257
+ # =====================
258
+ # FEATURE ENGINEERING (SESUAI CSV ANDA)
259
+ # =====================
260
+ df['daily_temp'] = df['temp'] # dari temp
261
+ df['daily_humidity_diff'] = df['humidity'].diff() # dari humidity
262
+ df['daily_precipprob_diff'] = df['precipprob'].diff() # dari precipprob
263
+ df = df.dropna()
264
+
265
+ # =====================
266
+ # FILTER FITUR SESUAI MODEL VAR
267
+ # =====================
268
+ model_features = [
269
+ 'daily_temp',
270
+ 'daily_humidity_diff',
271
+ 'daily_precipprob_diff'
272
+ ]
273
+
274
+ df = df[model_features]
275
+
276
+ # =====================
277
+ # AMBIL 3 HARI TERAKHIR
278
+ # =====================
279
+ last_3_days = df.tail(3)
280
+
281
+ # =====================
282
+ # FORECAST 5 HARI
283
+ # =====================
284
+ forecast_values = var_model.forecast(df.values, steps=5)
285
+
286
+ forecast_dates = [
287
+ df.index[-1] + pd.Timedelta(days=i)
288
+ for i in range(1, 6)
289
+ ]
290
+
291
+ forecast_df = pd.DataFrame(
292
+ forecast_values,
293
+ columns=df.columns,
294
+ index=forecast_dates
295
+ )
296
+
297
+ return jsonify({
298
+ "last_days": {
299
+ "dates": last_3_days.index.strftime('%Y-%m-%d').tolist(),
300
+ "values": last_3_days.values.tolist()
301
+ },
302
+ "forecast": {
303
+ "dates": forecast_df.index.strftime('%Y-%m-%d').tolist(),
304
+ "values": forecast_df.values.tolist()
305
+ },
306
+ "columns": df.columns.tolist()
307
+ })
308
+
309
+ except Exception as e:
310
+ return jsonify({"error": str(e)}), 500
311
+
312
+ @app.route('/about')
313
+ def about():
314
+ return render_template('about.html')
315
+ # ==============================================================================
316
+ # --- 5. ROUTE CLUSTERING PER KABUPATEN (Kode Anda) ---
317
+ # ==============================================================================
318
+
319
+ @app.route('/clustering/<kabupaten>')
320
+ def get_clustering_data(kabupaten):
321
+ # Logika Clustering Anda (Sudah benar dan menggunakan scaling)
322
+ try:
323
+ # PENGAMBILAN INPUT USER
324
+ tgl = int(request.args.get("tgl", 1))
325
+ bln = int(request.args.get("bln", 1))
326
+ tahun = int(request.args.get("tahun", 2024))
327
+
328
+ kabupaten_lower = kabupaten.lower()
329
+ kabupaten_title = kabupaten.title()
330
+
331
+ if kabupaten_title not in CSV_MAP:
332
+ return jsonify({"error": "Kabupaten tidak valid"}), 400
333
+
334
+ # AMBIL MODEL DAN SCALER DARI MEMORI
335
+ model_kmeans = LOADED_MODELS_CLUSTERING.get(kabupaten_lower)
336
+ scaler = LOADED_SCALERS.get(kabupaten_lower)
337
+
338
+ if model_kmeans is None:
339
+ return jsonify({"error": f"Model clustering untuk {kabupaten_title} tidak ditemukan/gagal dimuat."}), 500
340
+
341
+ if scaler is None:
342
+ return jsonify({"error": f"Scaler untuk {kabupaten_title} tidak ditemukan/gagal dimuat. Tidak dapat melakukan scaling."}), 500
343
+
344
+
345
+ # BACA CSV
346
+ try:
347
+ df = pd.read_csv(CSV_MAP[kabupaten_title])
348
+ except FileNotFoundError:
349
+ return jsonify({"error": f"Gagal memuat CSV: File data untuk {kabupaten_title} tidak ditemukan."}), 500
350
+
351
+ df["datetime"] = pd.to_datetime(df["datetime"])
352
+
353
+ # FILTER TANGGAL
354
+ selected = df[
355
+ (df["datetime"].dt.day == tgl) &
356
+ (df["datetime"].dt.month == bln) &
357
+ (df["datetime"].dt.year == tahun)
358
+ ]
359
+
360
+ if selected.empty:
361
+ return jsonify({"error": f"Data cuaca untuk {tgl}/{bln}/{tahun} di {kabupaten_title} tidak ditemukan"}), 404
362
+
363
+ # Daftar 5 Fitur untuk Scaling
364
+ fitur_yang_dipakai_model = [
365
+ "tempmax",
366
+ "precipprob",
367
+ "humidity",
368
+ "solarradiation",
369
+ "windspeed"
370
+ ]
371
+
372
+ # 1. Ambil data mentah (raw) dari CSV
373
+ fitur_utama_raw = selected[fitur_yang_dipakai_model].values
374
+
375
+ # 2. LAKUKAN SCALING
376
+ fitur_utama_scaled = scaler.transform(fitur_utama_raw)
377
+
378
+ # 3. PREDIKSI
379
+ cluster = int(model_kmeans.predict(fitur_utama_scaled)[0])
380
+
381
+ return jsonify({
382
+ "kabupaten": kabupaten_title,
383
+ "input_features_raw": selected[fitur_yang_dipakai_model].iloc[0].to_dict(),
384
+ "feature_names": fitur_yang_dipakai_model,
385
+ "predicted_cluster": cluster
386
+ })
387
+
388
+ except KeyError as e:
389
+ return jsonify({"error": f"Gagal memprediksi cluster: Kolom fitur hilang atau salah nama: {str(e)}. Pastikan 5 kolom fitur sudah benar."}), 500
390
+ except ValueError as e:
391
+ return jsonify({"error": f"Gagal memprediksi cluster: Kesalahan nilai input atau format data: {str(e)}."}), 500
392
+ except Exception as e:
393
+ return jsonify({"error": f"Gagal memprediksi cluster: Terjadi error tak terduga: {str(e)}"}), 500
394
+
395
+ @app.route("/cuaca", methods=["GET", "POST"])
396
+ def prediksi_cuaca():
397
+ if WEATHER_MODEL is None:
398
+ return "Error: Model CUACA tidak dimuat. Pastikan file model_cuaca.h5 tersedia di folder proyek.", 500
399
+
400
+ if request.method == "POST":
401
+ file = request.files.get("image")
402
+ if not file:
403
+ return render_template("cuaca.html", hasil=None, error="Tidak ada file yang diunggah.")
404
+
405
+ save_path = os.path.join("static", file.filename)
406
+ file.save(save_path)
407
+
408
+ # Preprocessing sesuai input model : (100x100)
409
+ img = load_img(save_path, target_size=(100, 100))
410
+ img = img_to_array(img) / 255.0
411
+ img = np.expand_dims(img, axis=0)
412
+
413
+ pred = WEATHER_MODEL.predict(img)
414
+ label_index = np.argmax(pred)
415
+ hasil = WEATHER_CLASSES[label_index]
416
+
417
+ return render_template("cuaca.html", hasil=hasil, img=save_path)
418
+
419
+ return render_template("cuaca.html", hasil=None)
420
+
421
+ # ==============================================================================
422
+ # --- BLOK RUNNING APLIKASI ---
423
+ # ==============================================================================
424
+
425
+ if __name__ == "__main__":
426
+ app.run(debug=True)
kmeans_model_bangkalan.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f6f3fbaca1ea2cba7ae78860acfa4ee51a9a2916c452ae2d9e832c3f5b45907
3
+ size 4871
kmeans_model_pamekasan.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2999aae30f3d041ee6209baac6769f53fb6415703645507d8784c3ad726a87f3
3
+ size 4871
kmeans_model_sampang.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9293be414bfe9b0a10210a05583fdfa4232d7b2832d7711dc68aded50b2a4639
3
+ size 4871
kmeans_model_sumenep.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:063be71278070b43c3e4143dd1dee7a27d66582adebc80436a6a442dfa48e9e6
3
+ size 4871
random_forest_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:375b222e3e16cfe89c59c0d5fb3354fee8c54fbf79fb38eca90c6b1de1ed906d
3
+ size 1799545
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Flask==3.1.2
2
+ joblib==1.4.2
3
+ pandas==2.3.3
4
+ statsmodels
scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fa7e8e6089051d280910e9993f0e4dd2bd6a10cc84ec0181734e6e890909fef6
3
+ size 1327
scaler_bangkalan.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5894d90377289d1440eea0ad9a4a02d163eb2542e0d655bdf986b2c261f646b2
3
+ size 1039
scaler_pamekasan.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f3a8c990c28d414e24d383e811883895f2caa557c9c480782097c482f3f556ef
3
+ size 1039
scaler_sampang.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1d32ed43f9a548ea82d80b2fa378e01f5838b95eba503b4ee360139cfc003ba5
3
+ size 1039
scaler_sumenep.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8c9ec088afa0263f4d936ea26f9551340e9cfd07a51fa11ebecf0f67518fc81b
3
+ size 1039
static/Madura.geojson ADDED
The diff for this file is too large to render. See raw diff
 
static/bangkalan.csv ADDED
The diff for this file is too large to render. See raw diff
 
static/pamekasan.csv ADDED
The diff for this file is too large to render. See raw diff
 
static/sampang.csv ADDED
The diff for this file is too large to render. See raw diff
 
static/sumenep.csv ADDED
The diff for this file is too large to render. See raw diff
 
static/user.png ADDED

Git LFS Details

  • SHA256: 234be6256cfe7d9b51ff5b914cae06cd311e0e20ba8c26c94ab96ca22ede57bd
  • Pointer size: 131 Bytes
  • Size of remote file: 167 kB
static/var_model_multivariate.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e003b6c6eaccf788a9827ccad15d32305b8844b3cf34ce1346f3fc2133752fe2
3
+ size 191187
templates/Crop.html ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Rekomendasi Tanaman</title>
5
+ <style>
6
+ /* --- Tata Letak Utama (Grid/Pusat) --- */
7
+ body {
8
+ font-family: Arial, sans-serif;
9
+ margin: 0;
10
+ padding: 0;
11
+ min-height: 100vh;
12
+ background-color: #f4f4f4;
13
+ display: flex; /* Menggunakan flexbox untuk konten utama */
14
+ flex-direction: column;
15
+ }
16
+
17
+ /* --- Navbar (Dipertahankan) --- */
18
+ .navbar {
19
+ display: flex;
20
+ justify-content: space-between;
21
+ align-items: center;
22
+ background: #1a73e8;
23
+ padding: 10px 20px;
24
+ color: white;
25
+ font-size: 15px;
26
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
27
+ }
28
+
29
+ .navbar .brand {
30
+ font-size: 18px;
31
+ font-weight: bold;
32
+ }
33
+
34
+ .navbar .menu {
35
+ list-style: none;
36
+ display: flex;
37
+ gap: 20px;
38
+ margin: 0;
39
+ padding: 0;
40
+ }
41
+
42
+ .navbar .menu li a {
43
+ color: white;
44
+ text-decoration: none;
45
+ font-weight: 500;
46
+ }
47
+
48
+ .navbar .menu li a:hover {
49
+ text-decoration: underline;
50
+ }
51
+
52
+ /* --- Konten Utama (Pusat) --- */
53
+ .main-content-wrapper {
54
+ flex-grow: 1;
55
+ padding: 40px 20px;
56
+ background-color: #cdecfe;
57
+ display: flex;
58
+ justify-content: center;
59
+ align-items: flex-start;
60
+ }
61
+
62
+ /* Container untuk Form dan Hasil */
63
+ .form-container {
64
+ width: 100%;
65
+ max-width: 500px;
66
+ padding: 30px;
67
+ background-color: #ffffff;
68
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
69
+ border-radius: 8px;
70
+ }
71
+
72
+ /* --- TATA LETAK 2 KOLOM BARU (DI DALAM FORM) --- */
73
+
74
+ /* Wrapper untuk 2 kolom. Secara default, tetap satu kolom (Flex-direction: column) */
75
+ .form-fields-grid {
76
+ display: flex;
77
+ flex-wrap: wrap; /* Memungkinkan item untuk pindah ke baris baru */
78
+ gap: 20px; /* Jarak antara kolom dan baris */
79
+ }
80
+
81
+ .form-fields-grid > div {
82
+ width: 100%; /* Default: Ambil seluruh lebar (1 kolom) */
83
+ margin-bottom: 0; /* Hapus margin-bottom dari div lama karena kita gunakan gap */
84
+ }
85
+
86
+ /* Media Query untuk Layar Lebar (Tablet ke atas, misalnya > 768px) */
87
+ @media (min-width: 768px) {
88
+ /* Set max-width form-container lebih besar untuk menampung 2 kolom */
89
+ .form-container {
90
+ max-width: 800px;
91
+ }
92
+
93
+ /* Atur lebar setiap input menjadi hampir 50% untuk 2 kolom */
94
+ .form-fields-grid > div {
95
+ /* calc(50% - (gap / 2)) untuk memastikan 2 kolom muat dengan gap di tengah */
96
+ width: calc(50% - 10px);
97
+ }
98
+ }
99
+ form div {
100
+ margin-bottom: 15px;
101
+ }
102
+ label {
103
+ display: block;
104
+ font-weight: bold;
105
+ margin-bottom: 5px;
106
+ font-size: 1em;
107
+ color: #555;
108
+ }
109
+ input[type="text"] {
110
+ width: 100%;
111
+ padding: 10px; /* Padding diperbesar */
112
+ box-sizing: border-box;
113
+ border: 1px solid #ccc;
114
+ border-radius: 4px;
115
+ font-size: 1em;
116
+ }
117
+ input[type="submit"] {
118
+ background-color: #1a73e8;
119
+ color: white;
120
+ padding: 12px 15px; /* Padding diperbesar */
121
+ border: none;
122
+ cursor: pointer;
123
+ width: 100%;
124
+ margin-top: 20px; /* Margin diperbesar */
125
+ border-radius: 4px;
126
+ font-size: 1.1em;
127
+ transition: background-color 0.3s;
128
+ }
129
+ input[type="submit"]:hover {
130
+ background-color: #0b4694;
131
+ }
132
+ hr {
133
+ border: 0;
134
+ height: 1px;
135
+ background: #e0e0e0;
136
+ margin: 25px 0;
137
+ }
138
+ </style>
139
+ </head>
140
+ <body>
141
+ <header>
142
+ <nav class="navbar">
143
+ <div class="brand">Madura weather</div>
144
+ <ul class="menu">
145
+ <li><a href="/">Homepage</a></li>
146
+ <li><a href="/crop">Crop Recomendation</a></li>
147
+ <li><a href="/cuaca">Image Prediction</a></li>
148
+ <li><a href="/about">About</a></li>
149
+ </ul>
150
+ </nav>
151
+ </header>
152
+ <div class="main-content-wrapper">
153
+ <div class="form-container">
154
+ <h1>Crop Recomendation</h1>
155
+ <p style="text-align: center; color: #666; margin-bottom: 25px">
156
+ Enter the parameters below to get recommended plant types..
157
+ </p>
158
+
159
+ {% if error_message %}
160
+ <p class="error">{{ error_message }}</p>
161
+ {% endif %}
162
+
163
+ <form action="{{ url_for('predict') }}" method="post">
164
+ <div class="form-fields-grid">
165
+ {% for name in feature_names %}
166
+ <div>
167
+ <label for="{{ name }}"
168
+ >{{ name.upper() if name | length < 3 else name.capitalize()
169
+ }}:</label>
170
+ <input
171
+ type="text"
172
+ id="{{ name }}"
173
+ name="{{ name }}"
174
+ required
175
+ value="{{ form_values.get(name, '') }}"
176
+ />
177
+ </div>
178
+ {% endfor %}
179
+ </div>
180
+ <input type="submit" value="Dapatkan Prediksi" />
181
+ </form>
182
+
183
+ <hr />
184
+ {% if prediction_text %}
185
+ <h2 class="prediction">Hasil Prediksi: {{ prediction_text }}</h2>
186
+ {% endif %}
187
+ </div>
188
+ </div>
189
+ <!-- <div class="main-content-wrapper">
190
+ <div class="form-container">
191
+ <h1>Rekomendasi Jenis Tanaman</h1>
192
+ <p style="text-align: center; color: #666; margin-bottom: 25px">
193
+ Masukkan parameter di bawah ini untuk mendapatkan rekomendasi jenis
194
+ tanaman.
195
+ </p>
196
+
197
+ {% if error_message %}
198
+ <p class="error">{{ error_message }}</p>
199
+ {% endif %}
200
+
201
+ <form action="{{ url_for('predict') }}" method="post">
202
+ {% for name in feature_names %}
203
+ <div>
204
+ <label for="{{ name }}"
205
+ >{{ name.upper() if name | length < 3 else name.capitalize()
206
+ }}:</label
207
+ >
208
+ <input
209
+ type="text"
210
+ id="{{ name }}"
211
+ name="{{ name }}"
212
+ required
213
+ value="{{ form_values.get(name, '') }}"
214
+ />
215
+ </div>
216
+ {% endfor %}
217
+
218
+ <input type="submit" value="Dapatkan Prediksi" />
219
+ </form>
220
+
221
+ <hr />
222
+
223
+ {% if prediction_text %}
224
+ <h2 class="prediction">Hasil Prediksi: {{ prediction_text }}</h2>
225
+ {% endif %}
226
+ </div>
227
+ </div> -->
228
+ </body>
229
+ </html>
templates/about.html ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Tentang Kami - Tim Kami</title>
7
+ <style>
8
+ /* --- Gaya Umum (Dari template sebelumnya) --- */
9
+ body {
10
+ font-family: Arial, sans-serif;
11
+ margin: 0;
12
+ padding: 0;
13
+ min-height: 100vh;
14
+ background-color: #f4f4f4;
15
+ display: flex;
16
+ flex-direction: column;
17
+ }
18
+
19
+ /* --- Navbar (Dipertahankan) --- */
20
+ .navbar {
21
+ display: flex;
22
+ justify-content: space-between;
23
+ align-items: center;
24
+ background: #1a73e8;
25
+ padding: 10px 20px;
26
+ color: white;
27
+ font-size: 15px;
28
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
29
+ }
30
+
31
+ .navbar .brand {
32
+ font-size: 18px;
33
+ font-weight: bold;
34
+ }
35
+
36
+ .navbar .menu {
37
+ list-style: none;
38
+ display: flex;
39
+ gap: 20px;
40
+ margin: 0;
41
+ padding: 0;
42
+ }
43
+
44
+ .navbar .menu li a {
45
+ color: white;
46
+ text-decoration: none;
47
+ font-weight: 500;
48
+ }
49
+
50
+ .navbar .menu li a:hover {
51
+ text-decoration: underline;
52
+ }
53
+
54
+ /* --- Konten Utama Halaman (Tentang Kami) --- */
55
+ .main-content-wrapper {
56
+ flex-grow: 1;
57
+ padding: 40px 20px;
58
+ background-color: #cdecfe;
59
+ display: flex;
60
+ justify-content: center;
61
+ align-items: flex-start;
62
+ }
63
+
64
+ .about-container {
65
+ width: 100%;
66
+ max-width: 1200px; /* Lebar lebih besar untuk 4 kartu */
67
+ padding: 30px;
68
+ background-color: #ffffff;
69
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
70
+ border-radius: 8px;
71
+ }
72
+
73
+ .about-container h1 {
74
+ color: #1a73e8;
75
+ border-bottom: 2px solid #e0e0e0;
76
+ padding-bottom: 15px;
77
+ margin-top: 0;
78
+ font-size: 2em;
79
+ text-align: center;
80
+ margin-bottom: 30px;
81
+ }
82
+
83
+ /* --- Gaya Kartu Tim (CSS Grid) --- */
84
+ .team-grid {
85
+ display: grid;
86
+ /* Default untuk mobile: 1 kolom */
87
+ grid-template-columns: 1fr;
88
+ gap: 30px; /* Jarak antara kartu */
89
+ padding: 10px;
90
+ }
91
+
92
+ .member-card {
93
+ background-color: #f8f8f8;
94
+ border-radius: 8px;
95
+ padding: 20px;
96
+ text-align: center;
97
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
98
+ transition: transform 0.3s, box-shadow 0.3s;
99
+ }
100
+
101
+ .member-card:hover {
102
+ transform: translateY(-5px);
103
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
104
+ }
105
+
106
+ .member-photo {
107
+ width: 120px;
108
+ height: 120px;
109
+ border-radius: 50%; /* Membuat gambar lingkaran */
110
+ object-fit: cover; /* Memastikan gambar terpotong rapi */
111
+ margin: 0 auto 15px auto;
112
+ border: 4px solid #1a73e8;
113
+ }
114
+
115
+ .member-card h3 {
116
+ margin: 5px 0 5px 0;
117
+ color: #1c4587;
118
+ font-size: 1.4em;
119
+ }
120
+
121
+ .member-card p.role {
122
+ color: #555;
123
+ font-style: italic;
124
+ margin-bottom: 15px;
125
+ font-size: 1em;
126
+ }
127
+
128
+ .member-card p.bio {
129
+ color: #777;
130
+ font-size: 0.9em;
131
+ text-align: left;
132
+ }
133
+
134
+ /* --- Media Queries untuk Responsif --- */
135
+
136
+ /* Untuk layar tablet (>= 600px) */
137
+ @media (min-width: 600px) {
138
+ .team-grid {
139
+ /* 2 kolom pada tablet */
140
+ grid-template-columns: 1fr 1fr;
141
+ }
142
+ }
143
+
144
+ /* Untuk layar desktop (>= 1024px) */
145
+ @media (min-width: 1024px) {
146
+ .team-grid {
147
+ /* 4 kolom pada desktop */
148
+ grid-template-columns: repeat(4, 1fr);
149
+ }
150
+ }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <header>
155
+ <nav class="navbar">
156
+ <div class="brand">Madura weather</div>
157
+ <ul class="menu">
158
+ <li><a href="/">Homepage</a></li>
159
+ <li><a href="/crop">Recomendation Plant</a></li>
160
+ <li><a href="/cuaca">Image Prediction</a></li>
161
+ <li><a href="/about">About</a></li>
162
+ </ul>
163
+ </nav>
164
+ </header>
165
+
166
+ <div class="main-content-wrapper">
167
+ <div class="about-container">
168
+ <h1>Our Team</h1>
169
+
170
+ <div class="team-grid">
171
+ <div class="member-card">
172
+ <img
173
+ src="{{ url_for('static', filename='user.png') }}"
174
+ alt="Foto Anggota 1"
175
+ class="member-photo"
176
+ />
177
+ <h3>Mochammad Febrianu Hakim Alamsyah</h3>
178
+ <!-- <p class="role">Lead Developer & Machine Learning</p>
179
+ <p class="bio">
180
+ Bertanggung jawab atas model Random Forest dan implementasi
181
+ backend Flask.
182
+ </p> -->
183
+ </div>
184
+
185
+ <div class="member-card">
186
+ <img
187
+ src="{{ url_for('static', filename='user.png') }}"
188
+ alt="Foto Anggota 2"
189
+ class="member-photo"
190
+ />
191
+ <h3>Muhammad Alivian Sidiq</h3>
192
+ <!-- <p class="role">Frontend Designer & UI/UX</p>
193
+ <p class="bio">
194
+ Memastikan antarmuka pengguna responsif, intuitif, dan mudah
195
+ digunakan oleh petani.
196
+ </p> -->
197
+ </div>
198
+
199
+ <div class="member-card">
200
+ <img
201
+ src="{{ url_for('static', filename='user.png') }}"
202
+ alt="Foto Anggota 3"
203
+ class="member-photo"
204
+ />
205
+ <h3>Nadhif Fajrul Minan</h3>
206
+ <!-- <p class="role">Data Scientist & Analis Pertanian</p>
207
+ <p class="bio">
208
+ Mengumpulkan, membersihkan data pertanian, dan memvalidasi akurasi
209
+ model prediksi.
210
+ </p> -->
211
+ </div>
212
+
213
+ <div class="member-card">
214
+ <img
215
+ src="{{ url_for('static', filename='user.png') }}"
216
+ alt="Foto Anggota 4"
217
+ class="member-photo"
218
+ />
219
+ <h3>Achmad Habibul Wildan Syaifulloh</h3>
220
+ <!-- <p class="role">Project Manager & Dokumentasi</p>
221
+ <p class="bio">
222
+ Mengkoordinasikan proyek, mengelola jadwal, dan menyiapkan
223
+ dokumentasi teknis.
224
+ </p> -->
225
+ </div>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </body>
230
+ </html>
templates/cuaca.html ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Prediksi Cuaca</title>
7
+ <!-- Memuat Tailwind CSS dari CDN -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ /* Menggunakan font Inter secara default */
12
+ body {
13
+ font-family: 'Inter', sans-serif;
14
+ min-height: 100vh;
15
+ /* Memastikan bg.png dimuat melalui url_for */
16
+ background-image: url('{{ url_for("static", filename="bg.png") }}');
17
+ background-size: cover;
18
+ background-position: center;
19
+ background-attachment: fixed;
20
+ }
21
+ /* Style untuk Overlay Transparan agar teks lebih mudah dibaca */
22
+ .overlay {
23
+ background-color: rgba(0, 0, 0, 0.6);
24
+ min-height: 100vh;
25
+ }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <!-- Overlay untuk Konten -->
30
+ <div class="overlay flex items-center justify-center p-4 sm:p-8">
31
+
32
+ <div class="w-full max-w-xl bg-white/90 backdrop-blur-sm rounded-xl shadow-2xl p-6 sm:p-10 transition-all duration-300">
33
+
34
+ <!-- Header dan Judul -->
35
+ <header class="text-center mb-8">
36
+ <div class="text-4xl text-blue-700 mb-2">
37
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10 inline-block align-middle">
38
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15A.75.75 0 0 0 3 15.75h18a.75.75 0 0 0 .75-.75V9a.75.75 0 0 0-.75-.75H3a.75.75 0 0 0-.75.75v6ZM3 9v-3h18v3M15 19.5h1.5M10.5 19.5h1.5M7.5 19.5h1.5" />
39
+ </svg>
40
+ </div>
41
+ <h1 class="text-3xl sm:text-4xl font-extrabold text-gray-800">Klasifikasi Gambar Cuaca</h1>
42
+ <p class="text-gray-600 mt-2">Upload citra cuaca untuk mendapatkan prediksi dari model Anda.</p>
43
+ </header>
44
+
45
+ <!-- Form Upload (Disesuaikan dengan action="/cuaca") -->
46
+ <form action="/cuaca" method="POST" enctype="multipart/form-data" class="space-y-6">
47
+
48
+ <div class="relative border-2 border-dashed border-blue-400 rounded-lg p-6 hover:border-blue-600 transition duration-200 cursor-pointer">
49
+ <!-- Label untuk tampilan file input -->
50
+ <label for="file_input" class="block text-center text-gray-500 hover:text-blue-600 transition duration-200">
51
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mx-auto mb-2">
52
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" />
53
+ </svg>
54
+ <span class="font-medium">Klik untuk memilih file atau seret & lepas di sini</span>
55
+ <p class="text-sm text-gray-400">PNG, JPG, JPEG</p>
56
+ </label>
57
+
58
+ <!-- Input file tersembunyi (Disesuaikan dengan name="image") -->
59
+ <input type="file" name="image" id="file_input" required class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
60
+ </div>
61
+
62
+ <button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold shadow-md hover:bg-blue-700 transition duration-200 transform hover:scale-[1.01] focus:outline-none focus:ring-4 focus:ring-blue-300">
63
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 inline-block mr-2 align-text-bottom">
64
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
65
+ </svg>
66
+ Prediksi
67
+ </button>
68
+ </form>
69
+
70
+ <!-- Bagian Hasil Prediksi (Disesuaikan dengan variabel 'hasil' dan 'img') -->
71
+ {% if hasil %}
72
+ <div class="mt-8 pt-6 border-t border-gray-200">
73
+ <h3 class="text-xl font-bold text-gray-800 mb-4 text-center">Hasil Klasifikasi</h3>
74
+
75
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-6 items-start">
76
+
77
+ <!-- Tampilan Gambar -->
78
+ <div class="flex flex-col items-center">
79
+ <p class="text-sm font-medium text-gray-600 mb-2">Gambar yang Diupload:</p>
80
+ <div class="relative w-full aspect-square overflow-hidden rounded-lg shadow-xl">
81
+ <img src="{{ img }}" alt="Gambar yang Diupload" class="w-full h-full object-cover transition-all duration-300 transform hover:scale-105">
82
+ <div class="absolute inset-0 bg-black/10"></div>
83
+ </div>
84
+ </div>
85
+
86
+ <!-- Detail Prediksi -->
87
+ <div class="flex flex-col justify-center h-full">
88
+ <div class="bg-blue-100 p-6 rounded-lg shadow-md">
89
+ <p class="text-sm font-medium text-blue-600 mb-1">HASIL PREDIKSI:</p>
90
+ <!-- Menampilkan variabel 'hasil' dari Flask -->
91
+ <p class="text-3xl font-extrabold text-blue-800 break-words">{{ hasil }}</p>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ {% endif %}
97
+
98
+ <!-- Blok Flash Messages (Dikomnetari karena tidak ada di kode asli, untuk menghindari error jika belum diimplementasikan di app.py) -->
99
+ {#
100
+ {% with messages = get_flashed_messages(with_categories=true) %}
101
+ {% if messages %}
102
+ <div class="mt-6 space-y-3">
103
+ {% for category, message in messages %}
104
+ <div class="p-3 rounded-lg text-sm {% if category == 'error' %}bg-red-100 text-red-700{% else %}bg-green-100 text-green-700{% endif %}">
105
+ {{ message }}
106
+ </div>
107
+ {% endfor %}
108
+ </div>
109
+ {% endif %}
110
+ {% endwith %}
111
+ #}
112
+ </div>
113
+ </div>
114
+
115
+ <script>
116
+ // Logika sederhana untuk menampilkan nama file yang dipilih
117
+ document.getElementById('file_input').addEventListener('change', function() {
118
+ const fileName = this.files[0] ? this.files[0].name : 'Klik untuk memilih file...';
119
+ const label = document.querySelector('label[for="file_input"]');
120
+ if (label) {
121
+ // Update elemen span
122
+ label.querySelector('span').textContent = fileName;
123
+ // Update elemen p
124
+ const pElement = label.querySelector('p');
125
+ if (pElement) {
126
+ pElement.textContent = 'File terpilih, siap diupload.';
127
+ }
128
+ }
129
+ });
130
+ </script>
131
+ </body>
132
+ </html>
templates/index.html ADDED
@@ -0,0 +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>