1819 lines
84 KiB
HTML
1819 lines
84 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="vi">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Land Classification Training Interface</title>
|
|
|
|
<!-- Leaflet CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
|
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
padding: 20px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 20px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5em;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.header p {
|
|
opacity: 0.9;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.content {
|
|
padding: 30px;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 30px;
|
|
}
|
|
|
|
#map {
|
|
height: 500px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.map-container {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.map-instructions {
|
|
background: #e3f2fd;
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
margin-bottom: 15px;
|
|
border-left: 4px solid #2196f3;
|
|
}
|
|
|
|
.map-instructions h3 {
|
|
color: #1976d2;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.map-instructions p {
|
|
color: #555;
|
|
margin: 5px 0;
|
|
}
|
|
|
|
.section {
|
|
margin-bottom: 30px;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.section h2 {
|
|
color: #667eea;
|
|
margin-bottom: 15px;
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
color: #333;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.form-group input, .form-group select {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 5px;
|
|
font-size: 1em;
|
|
transition: border-color 0.3s;
|
|
}
|
|
|
|
.form-group input:focus, .form-group select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 30px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-size: 1em;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.status-box {
|
|
padding: 20px;
|
|
background: white;
|
|
border-radius: 10px;
|
|
border-left: 5px solid #667eea;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.status-box.success {
|
|
border-left-color: #28a745;
|
|
background: #d4edda;
|
|
}
|
|
|
|
.status-box.error {
|
|
border-left-color: #dc3545;
|
|
background: #f8d7da;
|
|
}
|
|
|
|
.status-box.training {
|
|
border-left-color: #ffc107;
|
|
background: #fff3cd;
|
|
}
|
|
|
|
.progress {
|
|
height: 30px;
|
|
background: #e0e0e0;
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|
transition: width 0.3s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.preset-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.preset-btn {
|
|
padding: 8px 15px;
|
|
background: white;
|
|
border: 2px solid #667eea;
|
|
color: #667eea;
|
|
border-radius: 20px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.preset-btn:hover {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.info {
|
|
background: #e3f2fd;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin-bottom: 15px;
|
|
border-left: 4px solid #2196f3;
|
|
}
|
|
|
|
.model-list {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.model-item {
|
|
background: white;
|
|
padding: 15px;
|
|
margin-bottom: 10px;
|
|
border-radius: 5px;
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.model-item h3 {
|
|
color: #667eea;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.model-item p {
|
|
margin: 5px 0;
|
|
color: #666;
|
|
}
|
|
|
|
/* Animations for notifications */
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideOut {
|
|
from {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Training Interface</h1>
|
|
<p>Giao diện training model phân loại đất từ ảnh vệ tinh</p>
|
|
</div>
|
|
|
|
<div style="background: white; padding: 15px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; border-bottom: 2px solid #e0e0e0;">
|
|
<a href="/" style="padding: 10px 20px; background: #667eea; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🏠 Trang Chủ</a>
|
|
<a href="/training" style="padding: 10px 20px; background: #f093fb; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🎓 Training (Active)</a>
|
|
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</a>
|
|
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing</a>
|
|
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
|
|
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<!-- Status Section -->
|
|
<div class="section">
|
|
<h2>📊 Trạng Thái Training</h2>
|
|
<div id="statusBox" class="status-box">
|
|
<p><strong>Trạng thái:</strong> <span id="statusText">Chưa bắt đầu</span></p>
|
|
<p><strong>Tiến độ:</strong> <span id="progressText">-</span></p>
|
|
<div id="progressBar" class="progress" style="display: none;">
|
|
<div class="progress-bar" style="width: 0%;">0%</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Info -->
|
|
<div style="margin-top: 20px;">
|
|
<h3 style="color: #667eea; margin-bottom: 10px;">💻 Thông Tin Hệ Thống</h3>
|
|
<div class="info">
|
|
<p><strong>🖥️ GPU:</strong> <span id="gpuInfo">Đang tải...</span></p>
|
|
<p><strong>💾 RAM:</strong> <span id="ramInfo">Đang tải...</span></p>
|
|
<p><strong>📁 Models:</strong> <span id="modelCount">0</span> models đã train</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Training History -->
|
|
<div style="margin-top: 20px;">
|
|
<h3 style="color: #667eea; margin-bottom: 10px;">📜 Lịch Sử Training</h3>
|
|
<div id="trainingHistory" style="max-height: 300px; overflow-y: auto; background: #f8f9fa; padding: 15px; border-radius: 8px; font-size: 13px;">
|
|
<p style="color: #999;">Chưa có lịch sử training</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div style="margin-top: 20px;">
|
|
<h3 style="color: #667eea; margin-bottom: 10px;">📈 Thống Kê Nhanh</h3>
|
|
<div class="info" style="background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);">
|
|
<p><strong>🎯 Tổng số lần train:</strong> <span id="totalTrainings">0</span></p>
|
|
<p><strong>✅ Thành công:</strong> <span id="successTrainings">0</span></p>
|
|
<p><strong>❌ Thất bại:</strong> <span id="failedTrainings">0</span></p>
|
|
<p><strong>⏱️ Thời gian TB:</strong> <span id="avgTime">-</span></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Guide -->
|
|
<div style="margin-top: 20px;">
|
|
<h3 style="color: #667eea; margin-bottom: 10px;">📖 Hướng Dẫn Nhanh</h3>
|
|
<div class="info" style="background: #e7f3ff; border-left: 4px solid #2196F3;">
|
|
<ol style="margin: 0; padding-left: 20px; font-size: 13px;">
|
|
<li>Chọn preset hoặc vẽ khu vực trên bản đồ</li>
|
|
<li>Chọn thời gian và cấu hình vệ tinh</li>
|
|
<li>Điều chỉnh tham số model (nếu cần)</li>
|
|
<li>Nhấn "Bắt Đầu Training"</li>
|
|
<li>Theo dõi tiến độ ở phần này</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Configuration Section -->
|
|
<div class="section">
|
|
<h2>⚙️ Cấu Hình Training</h2>
|
|
|
|
<div class="info">
|
|
<strong>💡 Lựa chọn nhanh:</strong> Chọn một trong các preset bên dưới hoặc tùy chỉnh thủ công
|
|
</div>
|
|
|
|
<div class="preset-buttons" id="presetButtons">
|
|
<!-- Preset buttons will be inserted here -->
|
|
</div>
|
|
|
|
<form id="trainingForm">
|
|
<h3 style="margin-bottom: 15px; color: #667eea;">📍 Khu Vực Training</h3>
|
|
|
|
<!-- Province Selection Section -->
|
|
<div class="form-group" style="margin-bottom: 20px;">
|
|
<label>
|
|
<strong>🗺️ Chọn theo Tỉnh Thành:</strong>
|
|
<span style="color: #999; font-size: 13px; font-weight: normal;">(Hoặc vẽ bbox thủ công bên dưới)</span>
|
|
</label>
|
|
|
|
<!-- Toggle between 63 and 32 provinces -->
|
|
<div style="margin-bottom: 10px; display: flex; gap: 10px; align-items: center;">
|
|
<button type="button" id="btn63Provinces" onclick="switchProvinceList('63')" style="padding: 8px 16px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">63 Tỉnh (Cũ)</button>
|
|
<button type="button" id="btn32Provinces" onclick="switchProvinceList('32')" style="padding: 8px 16px; background: #f0f0f0; color: #333; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">32 Tỉnh (Sau sáp nhập)</button>
|
|
<span id="provinceListMode" style="color: #667eea; font-weight: bold;">Danh sách: 63 tỉnh</span>
|
|
</div>
|
|
|
|
<select id="provinceSelect" style="padding: 12px; width: 100%; border: 2px solid #ddd; border-radius: 8px; font-size: 14px; cursor: pointer;">
|
|
<option value="">-- Chọn tỉnh thành để tải bbox tự động --</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Region Filter -->
|
|
<div class="form-group" style="margin-bottom: 20px;">
|
|
<label><strong>🌍 Lọc theo Vùng:</strong></label>
|
|
<div id="regionFilterContainer" style="display: flex; gap: 10px; flex-wrap: wrap;">
|
|
<button type="button" class="region-filter-btn" data-region="all" style="padding: 8px 16px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">Tất cả</button>
|
|
<!-- Dynamic region buttons will be added here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="map-container">
|
|
<div class="map-instructions">
|
|
<strong>💡 Hướng dẫn:</strong> Chọn tỉnh thành ở trên để tự động điền bbox, hoặc sử dụng công cụ vẽ hình chữ nhật
|
|
<span style="display: inline-block; width: 24px; height: 24px; background: white; border: 2px solid #333; vertical-align: middle; margin: 0 5px;">□</span>
|
|
ở góc trên bên trái của bản đồ để vẽ khu vực tùy chỉnh
|
|
</div>
|
|
<div id="map"></div>
|
|
<div style="margin-top: 10px; font-size: 13px; color: #666;">
|
|
<strong>Khu vực đã chọn:</strong>
|
|
<span id="bboxDisplay">Chưa chọn khu vực</span>
|
|
<span id="provinceDisplay" style="margin-left: 10px; color: #667eea; font-weight: 600;"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden inputs to store bbox values -->
|
|
<input type="hidden" id="minLon" value="105.5" required>
|
|
<input type="hidden" id="minLat" value="9.2" required>
|
|
<input type="hidden" id="maxLon" value="106.4" required>
|
|
<input type="hidden" id="maxLat" value="10.0" required>
|
|
|
|
<h3 style="margin: 20px 0 15px; color: #667eea;">📅 Thời Gian</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>Ngày bắt đầu:</label>
|
|
<input type="date" id="startDate" value="2023-03-01" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Ngày kết thúc:</label>
|
|
<input type="date" id="endDate" value="2023-12-31" required>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 style="margin: 20px 0 15px; color: #667eea;">📊 Training Data (Shapefile)</h3>
|
|
<div class="form-group">
|
|
<label><strong>🗂️ Chọn file training shapefile:</strong></label>
|
|
<select id="trainingShapefile" style="font-size: 14px; font-weight: 600;">
|
|
<option value="">Đang tải...</option>
|
|
</select>
|
|
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
|
💡 Chọn shapefile chứa dữ liệu training points với labels
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Training Shapefile Info Display -->
|
|
<div id="shapefileInfo" style="display: none; margin-top: 15px; padding: 15px; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<h4 style="color: #1976d2; margin-bottom: 10px;">📋 Thông tin Shapefile</h4>
|
|
<div style="font-size: 13px;">
|
|
<p><strong>📍 Số điểm:</strong> <span id="shapefilePoints">-</span></p>
|
|
<p><strong>🏷️ Label column:</strong> <span id="shapefileLabelCol">-</span></p>
|
|
<p><strong>📊 Số lớp:</strong> <span id="shapefileLabelCount">-</span></p>
|
|
<p><strong>🗺️ Bbox:</strong> <span id="shapefileBbox">-</span></p>
|
|
</div>
|
|
|
|
<!-- Labels Distribution -->
|
|
<div id="labelsDistribution" style="margin-top: 10px;">
|
|
<h5 style="color: #1976d2; margin-bottom: 8px;">🎯 Phân bố Labels:</h5>
|
|
<div id="labelsList" style="font-size: 12px; max-height: 200px; overflow-y: auto;">
|
|
<!-- Labels will be inserted here -->
|
|
</div>
|
|
</div>
|
|
|
|
<button type="button" onclick="applyShapefileBbox()" style="margin-top: 10px; padding: 8px 16px; background: #2196F3; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">
|
|
📍 Áp dụng Bbox từ Shapefile
|
|
</button>
|
|
</div>
|
|
|
|
<h3 style="margin: 20px 0 15px; color: #667eea;">💾 Dataset Cache Preset</h3>
|
|
<div class="form-group">
|
|
<label>Chọn Dataset đã cache:</label>
|
|
<select id="cachePreset" style="font-size: 14px;">
|
|
<option value="">-- Không dùng cache preset --</option>
|
|
</select>
|
|
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
|
💡 Chọn dataset cache có sẵn để tự động điền các thông số tương ứng
|
|
</div>
|
|
</div>
|
|
|
|
<h3 style="margin: 20px 0 15px; color: #667eea;">�🛰️ Dữ Liệu Vệ Tinh</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>Số scenes tối đa:</label>
|
|
<input type="number" id="maxScenes" value="12" min="1" max="100" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Cloud cover (%):</label>
|
|
<input type="number" id="cloudCover" value="30" min="0" max="100" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Độ phân giải (m):</label>
|
|
<select id="resolution" required>
|
|
<option value="10">10m (Chính xác cao)</option>
|
|
<option value="20" selected>20m (Cân bằng)</option>
|
|
<option value="30">30m (Nhanh)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 style="margin: 20px 0 15px; color: #667eea;">🤖 Model Parameters</h3>
|
|
|
|
<!-- Model Type Selection -->
|
|
<div class="form-group">
|
|
<label><strong>🧠 Loại Model:</strong></label>
|
|
<select id="modelType" required style="font-weight: 600; font-size: 14px;">
|
|
<option value="xgboost" selected>🚀 XGBoost (Nhanh, Chính xác cao, Hỗ trợ GPU)</option>
|
|
<option value="random_forest">🌲 Random Forest (Ổn định, Không cần GPU)</option>
|
|
<option value="decision_tree">🌳 Decision Tree (Đơn giản, Nhanh nhất)</option>
|
|
<option value="svm">🎯 SVM (Chính xác, Chậm với dữ liệu lớn)</option>
|
|
<option value="cnn">🧠 CNN - Deep Learning (PyTorch, Tốt với ảnh vệ tinh, Hỗ trợ GPU)</option>
|
|
<option value="swin-unet">🌟 Swin-UNet (Transformer + U-Net, Độ chính xác cao, Hỗ trợ GPU)</option>
|
|
<option value="mobilenet-lraspp">📱 MobileNetV3 + LR-ASPP (Nhẹ, Nhanh, Semantic Segmentation, Hỗ trợ GPU)</option>
|
|
</select>
|
|
<div style="margin-top: 8px; padding: 10px; background: #e7f3ff; border-radius: 5px; font-size: 12px;">
|
|
<span id="modelTypeDesc" style="color: #1976d2;">
|
|
✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Model-specific parameters -->
|
|
<div class="form-row">
|
|
<div class="form-group" id="nEstimatorsGroup">
|
|
<label>N Estimators:</label>
|
|
<input type="number" id="nEstimators" value="100" min="10" max="1000" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Max Depth:</label>
|
|
<input type="number" id="maxDepth" value="20" min="1" max="50" required>
|
|
</div>
|
|
<div class="form-group" id="learningRateGroup">
|
|
<label>Learning Rate:</label>
|
|
<input type="number" step="0.0001" id="learningRate" value="0.1" min="0.0001" max="1" required>
|
|
</div>
|
|
<div class="form-group" id="testSizeGroup">
|
|
<label>Tỷ lệ dữ liệu test (0-1):</label>
|
|
<input type="number" step="0.05" id="testSize" value="0.2" min="0.05" max="0.5" required>
|
|
</div>
|
|
<div class="form-group" id="useGpuGroup">
|
|
<label>Use GPU:</label>
|
|
<select id="useGpu" required>
|
|
<option value="true" selected>Có (RTX 4060)</option>
|
|
<option value="false">Không (CPU)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cache Option -->
|
|
<div class="form-group" style="margin-top: 20px; padding: 15px; background: #fff8dc; border-radius: 8px; border: 1px solid #ffc107;">
|
|
<label style="display: flex; align-items: center; cursor: pointer; margin: 0;">
|
|
<input type="checkbox" id="useCache" checked style="width: 18px; height: 18px; margin-right: 10px;">
|
|
<span style="font-weight: 600; color: #856404;">💾 Sử dụng Cache Dataset</span>
|
|
</label>
|
|
<div style="font-size: 12px; color: #856404; margin-top: 8px; margin-left: 28px;">
|
|
✅ <strong>Khuyến nghị:</strong> Bật để test nhanh hơn. Lần đầu load dữ liệu sẽ chậm, nhưng các lần sau rất nhanh (không cần download lại từ satellite).<br>
|
|
📊 <span id="cacheInfo">Đang kiểm tra cache...</span>
|
|
</div>
|
|
<div style="margin-top: 10px; margin-left: 28px;">
|
|
<button type="button" class="btn btn-secondary" onclick="clearCache()" style="font-size: 12px; padding: 5px 10px; background: #dc3545;">
|
|
🗑️ Xóa Cache
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" onclick="loadCacheInfo()" style="font-size: 12px; padding: 5px 10px; background: #17a2b8; margin-left: 5px;">
|
|
🔄 Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 30px; text-align: center;">
|
|
<button type="submit" class="btn btn-primary" id="startBtn">
|
|
🚀 Bắt Đầu Training
|
|
</button>
|
|
<button type="button" class="btn btn-danger" id="stopBtn" style="display: none;">
|
|
⏹️ Dừng Training
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Models List Section -->
|
|
<div class="section">
|
|
<h2>📦 Danh Sách Models Đã Train</h2>
|
|
<button class="btn btn-secondary" onclick="loadModels()">🔄 Refresh</button>
|
|
<div id="modelsList" class="model-list" style="margin-top: 15px;">
|
|
<p>Đang tải...</p>
|
|
</div>
|
|
<div style="margin-top: 15px; visibility: hidden;">
|
|
<label for="selectedModel" style="font-weight:600; color:#667eea;">Chọn model để dự đoán:</label>
|
|
<select id="selectedModel" style="width:100%; padding:10px; border-radius:5px; font-size:1em; margin-top:5px;"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reports Section -->
|
|
<div class="section">
|
|
<h2>📝 Báo Cáo (Auto Reports)</h2>
|
|
<button class="btn btn-secondary" onclick="loadReports()">🔄 Refresh</button>
|
|
<div id="reportsList" class="model-list" style="margin-top: 15px;">
|
|
<p>Đang tải...</p>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Leaflet JavaScript -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
|
|
|
<script>
|
|
const API_BASE = 'http://localhost:8000/api';
|
|
let statusInterval = null;
|
|
let trainingHistory = [];
|
|
let trainingStats = {
|
|
total: 0,
|
|
success: 0,
|
|
failed: 0,
|
|
times: []
|
|
};
|
|
let trainingFiles = []; // Store training shapefile data
|
|
let currentShapefileData = null; // Currently selected shapefile data
|
|
|
|
// Load presets and models only after DOM is ready
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// Initialize map first - IMPORTANT!
|
|
initMap();
|
|
loadCacheInfo();
|
|
loadProvinces();
|
|
|
|
// Then load other data
|
|
await loadPresets();
|
|
await loadModels();
|
|
await loadReports();
|
|
await loadSystemInfo();
|
|
await loadTrainingFiles(); // Load training shapefiles - map must be ready
|
|
checkStatus();
|
|
loadTrainingHistory();
|
|
});
|
|
|
|
// Load training shapefile files
|
|
async function loadTrainingFiles() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/training/files`);
|
|
const data = await response.json();
|
|
|
|
const select = document.getElementById('trainingShapefile');
|
|
select.innerHTML = '<option value="">-- Chọn training shapefile --</option>';
|
|
|
|
if (data.files && data.files.length > 0) {
|
|
trainingFiles = data.files;
|
|
|
|
data.files.forEach(file => {
|
|
const option = document.createElement('option');
|
|
option.value = file.filename;
|
|
|
|
let displayText = file.filename;
|
|
if (file.point_count) {
|
|
displayText += ` (${file.point_count} points`;
|
|
if (file.label_count) {
|
|
displayText += `, ${file.label_count} classes`;
|
|
}
|
|
displayText += ')';
|
|
} else if (file.error) {
|
|
displayText += ' ⚠️ (Error)';
|
|
}
|
|
|
|
option.textContent = displayText;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
// Select default shapefile
|
|
const defaultFile = 'ST_training data_updated_1130points_new.shp';
|
|
const defaultOption = Array.from(select.options).find(opt => opt.value === defaultFile);
|
|
if (defaultOption) {
|
|
select.value = defaultFile;
|
|
await loadShapefileLabels(defaultFile);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading training files:', error);
|
|
document.getElementById('trainingShapefile').innerHTML = '<option value="">Lỗi tải danh sách files</option>';
|
|
}
|
|
}
|
|
|
|
// Load labels from selected shapefile
|
|
async function loadShapefileLabels(filename) {
|
|
if (!filename) {
|
|
document.getElementById('shapefileInfo').style.display = 'none';
|
|
currentShapefileData = null;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/training/shapefile/${encodeURIComponent(filename)}/labels`);
|
|
const data = await response.json();
|
|
|
|
currentShapefileData = data;
|
|
|
|
// Display shapefile info
|
|
document.getElementById('shapefilePoints').textContent = data.point_count || '-';
|
|
document.getElementById('shapefileLabelCol').textContent = data.label_column || '-';
|
|
document.getElementById('shapefileLabelCount').textContent = data.label_count || '-';
|
|
|
|
if (data.bbox) {
|
|
const bbox = data.bbox;
|
|
document.getElementById('shapefileBbox').textContent =
|
|
`[${bbox[0].toFixed(4)}, ${bbox[1].toFixed(4)}, ${bbox[2].toFixed(4)}, ${bbox[3].toFixed(4)}]`;
|
|
|
|
console.log('📦 Bbox from shapefile:', bbox);
|
|
console.log('🗺️ Map status:', map ? 'initialized' : 'NOT initialized');
|
|
console.log('📍 DrawnItems status:', drawnItems ? 'initialized' : 'NOT initialized');
|
|
|
|
// Auto-zoom map to shapefile bbox when selected
|
|
if (map && drawnItems) {
|
|
// Create bounds for Leaflet: [[south, west], [north, east]]
|
|
// bbox is [minx, miny, maxx, maxy] = [west, south, east, north]
|
|
const bounds = [[bbox[1], bbox[0]], [bbox[3], bbox[2]]];
|
|
|
|
console.log('🎯 Leaflet bounds to zoom:', bounds);
|
|
|
|
// Remove previous rectangle
|
|
if (currentRectangle) {
|
|
drawnItems.removeLayer(currentRectangle);
|
|
}
|
|
|
|
// Draw preview rectangle with light styling
|
|
currentRectangle = L.rectangle(bounds, {
|
|
color: '#9C27B0', // Purple color for preview
|
|
weight: 3,
|
|
fillOpacity: 0.2,
|
|
fillColor: '#9C27B0',
|
|
dashArray: '10, 5' // Dashed line to show it's preview
|
|
});
|
|
drawnItems.addLayer(currentRectangle);
|
|
console.log('✅ Rectangle drawn on map');
|
|
|
|
// Use setTimeout to ensure map is ready and give time for rendering
|
|
setTimeout(() => {
|
|
try {
|
|
console.log('🚀 Attempting flyToBounds...');
|
|
map.flyToBounds(bounds, {
|
|
padding: [80, 80],
|
|
duration: 1.5,
|
|
maxZoom: 11
|
|
});
|
|
console.log('✅ flyToBounds called successfully');
|
|
} catch (error) {
|
|
console.error('❌ Error during flyToBounds:', error);
|
|
}
|
|
}, 200); // Small delay to ensure everything is ready
|
|
} else {
|
|
console.error('❌ Cannot zoom: map or drawnItems not initialized!');
|
|
}
|
|
}
|
|
|
|
// Display labels distribution
|
|
const labelsList = document.getElementById('labelsList');
|
|
labelsList.innerHTML = '';
|
|
|
|
if (data.labels && data.labels.length > 0) {
|
|
data.labels.forEach(label => {
|
|
const labelDiv = document.createElement('div');
|
|
labelDiv.style.cssText = 'padding: 6px 10px; margin: 4px 0; background: white; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;';
|
|
|
|
const mappedIcon = label.mapped ? '✅' : '⚠️';
|
|
const mappedColor = label.mapped ? '#4caf50' : '#ff9800';
|
|
|
|
labelDiv.innerHTML = `
|
|
<span style="font-weight: 600;">${mappedIcon} ${label.name}</span>
|
|
<span style="color: ${mappedColor}; font-weight: 600;">Code: ${label.code} (${label.count} pts)</span>
|
|
`;
|
|
labelsList.appendChild(labelDiv);
|
|
});
|
|
}
|
|
|
|
document.getElementById('shapefileInfo').style.display = 'block';
|
|
|
|
} catch (error) {
|
|
console.error('Error loading shapefile labels:', error);
|
|
document.getElementById('shapefileInfo').style.display = 'none';
|
|
currentShapefileData = null;
|
|
}
|
|
}
|
|
|
|
// Apply bbox from selected shapefile
|
|
function applyShapefileBbox() {
|
|
if (!currentShapefileData || !currentShapefileData.bbox) {
|
|
alert('Không có bbox data từ shapefile');
|
|
return;
|
|
}
|
|
|
|
const bbox = currentShapefileData.bbox; // [minx, miny, maxx, maxy]
|
|
|
|
// Update bbox inputs
|
|
document.getElementById('minLon').value = bbox[0].toFixed(6);
|
|
document.getElementById('minLat').value = bbox[1].toFixed(6);
|
|
document.getElementById('maxLon').value = bbox[2].toFixed(6);
|
|
document.getElementById('maxLat').value = bbox[3].toFixed(6);
|
|
|
|
// Update bbox display
|
|
document.getElementById('bboxDisplay').textContent =
|
|
`Lon: ${bbox[0].toFixed(4)} → ${bbox[2].toFixed(4)}, Lat: ${bbox[1].toFixed(4)} → ${bbox[3].toFixed(4)}`;
|
|
document.getElementById('provinceDisplay').textContent = `📍 Từ Shapefile: ${currentShapefileData.filename}`;
|
|
|
|
// Update map with new bbox
|
|
if (map && drawnItems) {
|
|
// Create bounds for Leaflet: [[south, west], [north, east]]
|
|
const bounds = [[bbox[1], bbox[0]], [bbox[3], bbox[2]]];
|
|
|
|
// Remove previous rectangle if exists
|
|
if (currentRectangle) {
|
|
drawnItems.removeLayer(currentRectangle);
|
|
}
|
|
|
|
// Create and add new rectangle with distinctive styling
|
|
currentRectangle = L.rectangle(bounds, {
|
|
color: '#FF5722', // Orange color to distinguish from manually drawn
|
|
weight: 3,
|
|
fillOpacity: 0.25,
|
|
fillColor: '#FF9800'
|
|
});
|
|
drawnItems.addLayer(currentRectangle);
|
|
|
|
// Fit map to bounds with padding for better visibility
|
|
map.fitBounds(bounds, {
|
|
padding: [50, 50],
|
|
maxZoom: 12 // Don't zoom in too much
|
|
});
|
|
|
|
// Add animation effect
|
|
setTimeout(() => {
|
|
if (currentRectangle) {
|
|
currentRectangle.setStyle({
|
|
color: '#2196F3',
|
|
fillColor: '#2196F3'
|
|
});
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
// Show notification
|
|
showNotification('success', `✅ Đã áp dụng bbox từ shapefile: ${currentShapefileData.filename}\n📍 ${currentShapefileData.point_count} điểm training`);
|
|
}
|
|
|
|
// Notification helper
|
|
function showNotification(type, message) {
|
|
const notification = document.createElement('div');
|
|
const bgColor = type === 'success' ? '#28a745' : (type === 'error' ? '#dc3545' : '#ffc107');
|
|
notification.style.cssText = `position:fixed;top:20px;right:20px;background:${bgColor};color:white;padding:15px 20px;border-radius:8px;box-shadow:0 4px 6px rgba(0,0,0,0.1);z-index:10000;animation:slideIn 0.3s ease-out;`;
|
|
notification.textContent = message;
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.style.animation = 'slideOut 0.3s ease-out';
|
|
setTimeout(() => notification.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
// Load preset configurations
|
|
async function loadPresets() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/config/presets`);
|
|
const data = await response.json();
|
|
|
|
const container = document.getElementById('presetButtons');
|
|
container.innerHTML = '';
|
|
|
|
data.presets.forEach(preset => {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'preset-btn';
|
|
btn.textContent = preset.name;
|
|
btn.onclick = () => applyPreset(preset.config);
|
|
container.appendChild(btn);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading presets:', error);
|
|
}
|
|
}
|
|
|
|
// Apply preset configuration
|
|
function applyPreset(config) {
|
|
document.getElementById('minLon').value = config.min_lon;
|
|
document.getElementById('minLat').value = config.min_lat;
|
|
document.getElementById('maxLon').value = config.max_lon;
|
|
document.getElementById('maxLat').value = config.max_lat;
|
|
document.getElementById('startDate').value = config.start_date;
|
|
document.getElementById('endDate').value = config.end_date;
|
|
document.getElementById('maxScenes').value = config.max_scenes;
|
|
document.getElementById('cloudCover').value = config.cloud_cover;
|
|
document.getElementById('resolution').value = config.resolution;
|
|
if (config.test_size !== undefined) {
|
|
document.getElementById('testSize').value = config.test_size;
|
|
}
|
|
|
|
// Update map with new bounds
|
|
if (currentRectangle) {
|
|
drawnItems.removeLayer(currentRectangle);
|
|
}
|
|
|
|
const bounds = [[config.min_lat, config.min_lon], [config.max_lat, config.max_lon]];
|
|
const rectangle = L.rectangle(bounds, {
|
|
color: '#667eea',
|
|
weight: 3,
|
|
fillOpacity: 0.2
|
|
});
|
|
|
|
drawnItems.addLayer(rectangle);
|
|
currentRectangle = rectangle;
|
|
map.fitBounds(bounds);
|
|
updateBboxFromMap(L.latLngBounds(bounds));
|
|
}
|
|
|
|
// Handle form submission
|
|
document.getElementById('trainingForm').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
|
|
// Get selected training shapefile
|
|
const selectedShapefile = document.getElementById('trainingShapefile').value;
|
|
const trainingShapefile = selectedShapefile || 'train/ST_training data_updated_1130points_new.shp';
|
|
|
|
const config = {
|
|
min_lon: parseFloat(document.getElementById('minLon').value),
|
|
min_lat: parseFloat(document.getElementById('minLat').value),
|
|
max_lon: parseFloat(document.getElementById('maxLon').value),
|
|
max_lat: parseFloat(document.getElementById('maxLat').value),
|
|
start_date: document.getElementById('startDate').value,
|
|
end_date: document.getElementById('endDate').value,
|
|
max_scenes: parseInt(document.getElementById('maxScenes').value),
|
|
cloud_cover: parseInt(document.getElementById('cloudCover').value),
|
|
resolution: parseInt(document.getElementById('resolution').value),
|
|
model_type: document.getElementById('modelType').value,
|
|
n_estimators: parseInt(document.getElementById('nEstimators').value),
|
|
max_depth: parseInt(document.getElementById('maxDepth').value),
|
|
learning_rate: parseFloat(document.getElementById('learningRate').value),
|
|
test_size: parseFloat(document.getElementById('testSize').value),
|
|
use_gpu: document.getElementById('useGpu').value === 'true',
|
|
use_cache: document.getElementById('useCache').checked,
|
|
training_shapefile: trainingShapefile
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/training/start`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(config)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
alert('Lỗi: ' + error.detail);
|
|
return;
|
|
}
|
|
|
|
const result = await response.json();
|
|
alert(result.message);
|
|
|
|
// Start monitoring status
|
|
if (statusInterval) clearInterval(statusInterval);
|
|
statusInterval = setInterval(checkStatus, 2000);
|
|
|
|
document.getElementById('startBtn').style.display = 'none';
|
|
document.getElementById('stopBtn').style.display = 'inline-block';
|
|
|
|
} catch (error) {
|
|
alert('Lỗi kết nối: ' + error.message);
|
|
}
|
|
};
|
|
|
|
// Stop training
|
|
document.getElementById('stopBtn').onclick = async () => {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/training/stop`, {method: 'POST'});
|
|
const result = await response.json();
|
|
alert(result.message);
|
|
checkStatus();
|
|
} catch (error) {
|
|
alert('Lỗi: ' + error.message);
|
|
}
|
|
};
|
|
|
|
// Check training status
|
|
async function checkStatus() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/training/status`);
|
|
const status = await response.json();
|
|
|
|
const statusBox = document.getElementById('statusBox');
|
|
const statusText = document.getElementById('statusText');
|
|
const progressText = document.getElementById('progressText');
|
|
|
|
statusText.textContent = status.is_training ? 'Đang training...' :
|
|
(status.error ? 'Lỗi' : (status.result ? 'Hoàn thành' : 'Chờ'));
|
|
progressText.textContent = status.progress || '-';
|
|
|
|
// Update status box styling
|
|
statusBox.className = 'status-box';
|
|
if (status.is_training) {
|
|
statusBox.classList.add('training');
|
|
} else if (status.error) {
|
|
statusBox.classList.add('error');
|
|
} else if (status.result) {
|
|
statusBox.classList.add('success');
|
|
}
|
|
|
|
// Show/hide buttons
|
|
if (status.is_training) {
|
|
document.getElementById('startBtn').style.display = 'none';
|
|
document.getElementById('stopBtn').style.display = 'inline-block';
|
|
} else {
|
|
document.getElementById('startBtn').style.display = 'inline-block';
|
|
document.getElementById('stopBtn').style.display = 'none';
|
|
|
|
if (statusInterval) {
|
|
clearInterval(statusInterval);
|
|
statusInterval = null;
|
|
|
|
// Add to history when training completes
|
|
if (status.result || status.error) {
|
|
const record = {
|
|
timestamp: new Date().toLocaleString('vi-VN'),
|
|
success: !!status.result,
|
|
accuracy: status.result?.test_accuracy,
|
|
error: status.error,
|
|
duration: status.start_time && status.end_time ?
|
|
(new Date(status.end_time) - new Date(status.start_time)) / 1000 : null
|
|
};
|
|
addTrainingRecord(record);
|
|
}
|
|
}
|
|
|
|
if (status.result) {
|
|
await loadModels();
|
|
await loadReports();
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error checking status:', error);
|
|
}
|
|
}
|
|
|
|
// Load models list
|
|
async function loadModels() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/models/list`);
|
|
const data = await response.json();
|
|
|
|
const container = document.getElementById('modelsList');
|
|
const modelSelect = document.getElementById('selectedModel');
|
|
|
|
if (data.models.length === 0) {
|
|
container.innerHTML = '<p>Chưa có model nào</p>';
|
|
modelSelect.innerHTML = '<option value="">-- Chưa có model --</option>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
modelSelect.innerHTML = '<option value="">-- Chọn model --</option>';
|
|
|
|
data.models.forEach(model => {
|
|
// Add to models list display
|
|
const item = document.createElement('div');
|
|
item.className = 'model-item';
|
|
item.innerHTML = `
|
|
<h3>📦 ${model.filename}</h3>
|
|
<p><strong>Tạo lúc:</strong> ${new Date(model.created).toLocaleString('vi-VN')}</p>
|
|
<p><strong>Kích thước:</strong> ${model.size_mb} MB</p>
|
|
${model.info && model.info.train_accuracy ? `<p><strong>Train Accuracy:</strong> ${(model.info.train_accuracy * 100).toFixed(2)}%</p>` : ''}
|
|
${model.info && model.info.test_accuracy ? `<p><strong>Test Accuracy:</strong> ${(model.info.test_accuracy * 100).toFixed(2)}%</p>` : ''}
|
|
`;
|
|
container.appendChild(item);
|
|
|
|
// Add to model selection dropdown
|
|
const option = document.createElement('option');
|
|
option.value = model.filename;
|
|
option.textContent = `${model.filename} (${model.size_mb} MB)`;
|
|
modelSelect.appendChild(option);
|
|
});
|
|
|
|
// Update model count
|
|
document.getElementById('modelCount').textContent = data.models.length;
|
|
|
|
} catch (error) {
|
|
console.error('Error loading models:', error);
|
|
document.getElementById('modelsList').innerHTML = '<p>Lỗi khi tải danh sách models</p>';
|
|
}
|
|
}
|
|
|
|
// Load reports list
|
|
async function loadReports() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/reports/list`);
|
|
const data = await response.json();
|
|
|
|
const container = document.getElementById('reportsList');
|
|
|
|
if (data.reports.length === 0) {
|
|
container.innerHTML = '<p style="color: #666;">Chưa có báo cáo nào. Báo cáo sẽ tự động tạo sau mỗi lần training/prediction.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
|
|
data.reports.forEach(report => {
|
|
const item = document.createElement('div');
|
|
item.className = 'model-item';
|
|
|
|
const typeIcon = report.type === 'training' ? '📊' : '🗺️';
|
|
const typeLabel = report.type === 'training' ? 'Training Report' : 'Prediction Report';
|
|
const typeBg = report.type === 'training' ? '#667eea' : '#ff6b6b';
|
|
|
|
item.innerHTML = `
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<div>
|
|
<h3 style="margin-bottom: 8px;">
|
|
${typeIcon} ${report.filename}
|
|
</h3>
|
|
<p style="margin: 0;">
|
|
<span style="background: ${typeBg}; color: white; padding: 3px 10px; border-radius: 12px; font-size: 11px;">${typeLabel}</span>
|
|
</p>
|
|
<p style="margin-top: 8px; font-size: 13px; color: #666;">
|
|
<strong>Tạo lúc:</strong> ${new Date(report.created).toLocaleString('vi-VN')} |
|
|
<strong>Kích thước:</strong> ${report.size_kb} KB
|
|
</p>
|
|
</div>
|
|
<div style="display: flex; gap: 8px;">
|
|
<button onclick="viewReport('${report.filename}')" class="btn btn-primary" style="padding: 8px 15px; font-size: 13px;">
|
|
👁️ Xem
|
|
</button>
|
|
<button onclick="downloadReport('${report.filename}')" class="btn btn-secondary" style="padding: 8px 15px; font-size: 13px;">
|
|
📥 Tải
|
|
</button>
|
|
<button onclick="deleteReport('${report.filename}')" class="btn btn-danger" style="padding: 8px 15px; font-size: 13px;">
|
|
🗑️
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.appendChild(item);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading reports:', error);
|
|
document.getElementById('reportsList').innerHTML = '<p style="color: red;">Lỗi khi tải danh sách báo cáo</p>';
|
|
}
|
|
}
|
|
|
|
// View report in new tab
|
|
function viewReport(filename) {
|
|
window.open(`${API_BASE}/reports/view/${filename}`, '_blank');
|
|
}
|
|
|
|
// Download report
|
|
function downloadReport(filename) {
|
|
window.location.href = `${API_BASE}/reports/download/${filename}`;
|
|
}
|
|
|
|
// Delete report
|
|
async function deleteReport(filename) {
|
|
if (!confirm(`Bạn có chắc muốn xóa báo cáo: ${filename}?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/reports/delete/${filename}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showNotification('success', 'Đã xóa báo cáo thành công!');
|
|
loadReports();
|
|
} else {
|
|
showNotification('error', 'Không thể xóa báo cáo!');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting report:', error);
|
|
showNotification('error', 'Lỗi khi xóa báo cáo!');
|
|
}
|
|
}
|
|
|
|
// Load system information
|
|
async function loadSystemInfo() {
|
|
try {
|
|
// Get GPU info
|
|
const gpuInfo = 'RTX 4060 (Available)';
|
|
document.getElementById('gpuInfo').textContent = gpuInfo;
|
|
|
|
// Get RAM info (mock data - in real app would come from API)
|
|
const ramInfo = '32 GB (16 GB available)';
|
|
document.getElementById('ramInfo').textContent = ramInfo;
|
|
|
|
} catch (error) {
|
|
console.error('Error loading system info:', error);
|
|
document.getElementById('gpuInfo').textContent = 'N/A';
|
|
document.getElementById('ramInfo').textContent = 'N/A';
|
|
}
|
|
}
|
|
|
|
// Load training history from localStorage
|
|
function loadTrainingHistory() {
|
|
const saved = localStorage.getItem('trainingHistory');
|
|
if (saved) {
|
|
trainingHistory = JSON.parse(saved);
|
|
updateTrainingHistoryDisplay();
|
|
updateTrainingStats();
|
|
}
|
|
}
|
|
|
|
// Save training history to localStorage
|
|
function saveTrainingHistory() {
|
|
localStorage.setItem('trainingHistory', JSON.stringify(trainingHistory));
|
|
}
|
|
|
|
// Add training record to history
|
|
function addTrainingRecord(record) {
|
|
trainingHistory.unshift(record);
|
|
if (trainingHistory.length > 20) {
|
|
trainingHistory = trainingHistory.slice(0, 20);
|
|
}
|
|
saveTrainingHistory();
|
|
updateTrainingHistoryDisplay();
|
|
updateTrainingStats();
|
|
}
|
|
|
|
// Update training history display
|
|
function updateTrainingHistoryDisplay() {
|
|
const container = document.getElementById('trainingHistory');
|
|
|
|
if (trainingHistory.length === 0) {
|
|
container.innerHTML = '<p style="color: #999;">Chưa có lịch sử training</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = trainingHistory.map(record => {
|
|
const statusIcon = record.success ? '✅' : '❌';
|
|
const statusColor = record.success ? '#4caf50' : '#f44336';
|
|
const duration = record.duration ? ` (${Math.round(record.duration / 60)}m)` : '';
|
|
|
|
return `
|
|
<div style="padding: 8px; margin-bottom: 8px; background: white; border-radius: 4px; border-left: 3px solid ${statusColor};">
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<span style="font-weight: 600;">${statusIcon} ${record.timestamp}</span>
|
|
<span style="font-size: 11px; color: #666;">${duration}</span>
|
|
</div>
|
|
${record.accuracy ? `<div style="font-size: 12px; color: #666; margin-top: 4px;">Accuracy: ${(record.accuracy * 100).toFixed(1)}%</div>` : ''}
|
|
${record.error ? `<div style="font-size: 11px; color: #f44336; margin-top: 4px;">${record.error}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Update training statistics
|
|
function updateTrainingStats() {
|
|
const total = trainingHistory.length;
|
|
const success = trainingHistory.filter(r => r.success).length;
|
|
const failed = total - success;
|
|
|
|
document.getElementById('totalTrainings').textContent = total;
|
|
document.getElementById('successTrainings').textContent = success;
|
|
document.getElementById('failedTrainings').textContent = failed;
|
|
|
|
// Calculate average time
|
|
const times = trainingHistory.filter(r => r.duration).map(r => r.duration);
|
|
if (times.length > 0) {
|
|
const avgSeconds = times.reduce((a, b) => a + b, 0) / times.length;
|
|
const avgMinutes = Math.round(avgSeconds / 60);
|
|
document.getElementById('avgTime').textContent = `${avgMinutes} phút`;
|
|
} else {
|
|
document.getElementById('avgTime').textContent = '-';
|
|
}
|
|
}
|
|
|
|
// Initialize Leaflet Map
|
|
let map, drawnItems, currentRectangle;
|
|
|
|
function initMap() {
|
|
// Initialize map centered on Vietnam Mekong Delta
|
|
map = L.map('map').setView([9.55, 105.9], 9);
|
|
|
|
// Add OpenStreetMap tile layer
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors',
|
|
maxZoom: 18
|
|
}).addTo(map);
|
|
|
|
// Initialize feature group for drawn items
|
|
drawnItems = new L.FeatureGroup();
|
|
map.addLayer(drawnItems);
|
|
|
|
// Initialize draw control with only rectangle tool
|
|
const drawControl = new L.Control.Draw({
|
|
draw: {
|
|
polyline: false,
|
|
polygon: false,
|
|
circle: false,
|
|
marker: false,
|
|
circlemarker: false,
|
|
rectangle: {
|
|
shapeOptions: {
|
|
color: '#667eea',
|
|
weight: 3,
|
|
fillOpacity: 0.2
|
|
}
|
|
}
|
|
},
|
|
edit: {
|
|
featureGroup: drawnItems,
|
|
remove: true
|
|
}
|
|
});
|
|
map.addControl(drawControl);
|
|
|
|
// Handle rectangle creation
|
|
map.on(L.Draw.Event.CREATED, function(event) {
|
|
const layer = event.layer;
|
|
|
|
// Remove previous rectangle if exists
|
|
if (currentRectangle) {
|
|
drawnItems.removeLayer(currentRectangle);
|
|
}
|
|
|
|
// Add new rectangle
|
|
drawnItems.addLayer(layer);
|
|
currentRectangle = layer;
|
|
|
|
// Get bounds and update form
|
|
const bounds = layer.getBounds();
|
|
updateBboxFromMap(bounds);
|
|
});
|
|
|
|
// Handle rectangle edit
|
|
map.on(L.Draw.Event.EDITED, function(event) {
|
|
const layers = event.layers;
|
|
layers.eachLayer(function(layer) {
|
|
const bounds = layer.getBounds();
|
|
updateBboxFromMap(bounds);
|
|
});
|
|
});
|
|
|
|
// Handle rectangle deletion
|
|
map.on(L.Draw.Event.DELETED, function() {
|
|
currentRectangle = null;
|
|
document.getElementById('bboxDisplay').textContent = 'Chưa chọn khu vực';
|
|
// Reset to default values
|
|
document.getElementById('minLon').value = '';
|
|
document.getElementById('minLat').value = '';
|
|
document.getElementById('maxLon').value = '';
|
|
document.getElementById('maxLat').value = '';
|
|
});
|
|
|
|
// Draw initial rectangle based on default values
|
|
drawInitialRectangle();
|
|
}
|
|
|
|
function updateBboxFromMap(bounds) {
|
|
const south = bounds.getSouth().toFixed(6);
|
|
const west = bounds.getWest().toFixed(6);
|
|
const north = bounds.getNorth().toFixed(6);
|
|
const east = bounds.getEast().toFixed(6);
|
|
|
|
document.getElementById('minLat').value = south;
|
|
document.getElementById('minLon').value = west;
|
|
document.getElementById('maxLat').value = north;
|
|
document.getElementById('maxLon').value = east;
|
|
|
|
document.getElementById('bboxDisplay').textContent =
|
|
`Lon: ${west} → ${east}, Lat: ${south} → ${north}`;
|
|
}
|
|
|
|
function drawInitialRectangle() {
|
|
const minLon = parseFloat(document.getElementById('minLon').value);
|
|
const minLat = parseFloat(document.getElementById('minLat').value);
|
|
const maxLon = parseFloat(document.getElementById('maxLon').value);
|
|
const maxLat = parseFloat(document.getElementById('maxLat').value);
|
|
|
|
if (minLon && minLat && maxLon && maxLat) {
|
|
const bounds = [[minLat, minLon], [maxLat, maxLon]];
|
|
const rectangle = L.rectangle(bounds, {
|
|
color: '#667eea',
|
|
weight: 3,
|
|
fillOpacity: 0.2
|
|
});
|
|
|
|
drawnItems.addLayer(rectangle);
|
|
currentRectangle = rectangle;
|
|
map.fitBounds(bounds);
|
|
updateBboxFromMap(L.latLngBounds(bounds));
|
|
}
|
|
}
|
|
|
|
// Model type change handler
|
|
function updateModelTypeUI() {
|
|
const modelType = document.getElementById('modelType').value;
|
|
const desc = document.getElementById('modelTypeDesc');
|
|
const nEstimatorsGroup = document.getElementById('nEstimatorsGroup');
|
|
const learningRateGroup = document.getElementById('learningRateGroup');
|
|
const useGpuGroup = document.getElementById('useGpuGroup');
|
|
|
|
const descriptions = {
|
|
'xgboost': '✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh',
|
|
'random_forest': '✓ Random Forest: Ổn định, không overfitting, phù hợp mọi kích thước dữ liệu',
|
|
'decision_tree': '✓ Decision Tree: Đơn giản nhất, nhanh nhất, dễ hiểu, phù hợp để test nhanh',
|
|
'svm': '✓ SVM: Chính xác cao với dữ liệu nhỏ, chậm với dữ liệu lớn',
|
|
'cnn': '✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch',
|
|
'swin-unet': '✓ Swin-UNet: Kết hợp Transformer + U-Net, độ chính xác cao nhất, phù hợp dataset lớn, tốc độ training trung bình',
|
|
'mobilenet-lraspp': '✓ MobileNetV3 + LR-ASPP: Kiến trúc nhẹ cho semantic segmentation, nhanh, hiệu quả, phù hợp edge devices, hỗ trợ GPU'
|
|
};
|
|
|
|
desc.textContent = descriptions[modelType];
|
|
|
|
// Show/hide parameters based on model type
|
|
if (modelType === 'xgboost') {
|
|
nEstimatorsGroup.style.display = '';
|
|
learningRateGroup.style.display = '';
|
|
useGpuGroup.style.display = '';
|
|
} else if (modelType === 'random_forest') {
|
|
nEstimatorsGroup.style.display = '';
|
|
learningRateGroup.style.display = 'none';
|
|
useGpuGroup.style.display = 'none';
|
|
} else if (modelType === 'decision_tree') {
|
|
nEstimatorsGroup.style.display = 'none';
|
|
learningRateGroup.style.display = 'none';
|
|
useGpuGroup.style.display = 'none';
|
|
} else if (modelType === 'svm') {
|
|
nEstimatorsGroup.style.display = 'none';
|
|
learningRateGroup.style.display = 'none';
|
|
useGpuGroup.style.display = 'none';
|
|
} else if (modelType === 'cnn') {
|
|
// CNN uses n_estimators as epochs and supports GPU
|
|
nEstimatorsGroup.style.display = '';
|
|
document.querySelector('#nEstimatorsGroup label').textContent = 'Epochs (số lần training):';
|
|
document.getElementById('nEstimators').value = 50;
|
|
learningRateGroup.style.display = 'none';
|
|
useGpuGroup.style.display = ''; // Show GPU option for CNN
|
|
} else if (modelType === 'swin-unet') {
|
|
// Swin-UNet uses n_estimators as epochs and supports GPU
|
|
nEstimatorsGroup.style.display = '';
|
|
document.querySelector('#nEstimatorsGroup label').textContent = 'Epochs (số lần training):';
|
|
document.getElementById('nEstimators').value = 100;
|
|
learningRateGroup.style.display = ''; // Show learning rate for Swin-UNet
|
|
document.querySelector('#learningRateGroup label').textContent = 'Learning Rate (mặc định: 0.0005):';
|
|
document.getElementById('learningRate').value = 0.0005;
|
|
useGpuGroup.style.display = ''; // Show GPU option for Swin-UNet
|
|
} else if (modelType === 'mobilenet-lraspp') {
|
|
// MobileNetV3 + LR-ASPP uses n_estimators as epochs and supports GPU
|
|
nEstimatorsGroup.style.display = '';
|
|
document.querySelector('#nEstimatorsGroup label').textContent = 'Epochs (số lần training):';
|
|
document.getElementById('nEstimators').value = 60;
|
|
learningRateGroup.style.display = ''; // Show learning rate for MobileNet
|
|
document.querySelector('#learningRateGroup label').textContent = 'Learning Rate (mặc định: 0.001):';
|
|
document.getElementById('learningRate').value = 0.001;
|
|
useGpuGroup.style.display = ''; // Show GPU option for MobileNet
|
|
}
|
|
|
|
// Reset n_estimators label for non-CNN/non-Swin-UNet/non-MobileNet
|
|
if (modelType !== 'cnn' && modelType !== 'swin-unet' && modelType !== 'mobilenet-lraspp' && modelType !== 'decision_tree' && modelType !== 'svm') {
|
|
document.querySelector('#nEstimatorsGroup label').textContent = 'N Estimators:';
|
|
}
|
|
}
|
|
|
|
// ============== CACHE MANAGEMENT ==============
|
|
|
|
let cacheFiles = []; // Store cache files data
|
|
|
|
// Load cache info and populate preset dropdown
|
|
async function loadCacheInfo() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/cache/info`);
|
|
const data = await response.json();
|
|
|
|
// Update cache info display
|
|
const cacheInfoSpan = document.getElementById('cacheInfo');
|
|
if (data.exists && data.count > 0) {
|
|
cacheInfoSpan.innerHTML = `✅ Có <strong>${data.count}</strong> file cache (${data.total_size_mb} MB)`;
|
|
cacheInfoSpan.style.color = '#28a745';
|
|
} else {
|
|
cacheInfoSpan.innerHTML = '❌ Chưa có cache nào';
|
|
cacheInfoSpan.style.color = '#6c757d';
|
|
}
|
|
|
|
// Populate cache preset dropdown
|
|
const cachePreset = document.getElementById('cachePreset');
|
|
cachePreset.innerHTML = '<option value="">-- Không dùng cache preset --</option>';
|
|
|
|
if (data.exists && data.files && data.files.length > 0) {
|
|
cacheFiles = data.files;
|
|
data.files.forEach((file, index) => {
|
|
const meta = file.metadata;
|
|
const option = document.createElement('option');
|
|
option.value = index;
|
|
|
|
// Format display text with metadata
|
|
let displayText = `Cache #${index + 1}`;
|
|
if (meta && meta.n_samples) {
|
|
displayText += ` (${meta.n_samples} mẫu`;
|
|
if (meta.start_date && meta.end_date) {
|
|
displayText += `, ${meta.start_date} → ${meta.end_date}`;
|
|
}
|
|
if (meta.resolution) {
|
|
displayText += `, ${meta.resolution}m`;
|
|
}
|
|
displayText += ')';
|
|
} else {
|
|
displayText += ` (${file.size_mb} MB, ${new Date(file.modified).toLocaleString('vi-VN')})`;
|
|
}
|
|
|
|
option.textContent = displayText;
|
|
cachePreset.appendChild(option);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading cache info:', error);
|
|
document.getElementById('cacheInfo').innerHTML = '⚠️ Lỗi kiểm tra cache';
|
|
}
|
|
}
|
|
|
|
// Apply cache preset to form inputs
|
|
function applyCachePreset() {
|
|
const selectIndex = document.getElementById('cachePreset').value;
|
|
|
|
if (selectIndex === '') {
|
|
// Clear preset - no auto-fill
|
|
return;
|
|
}
|
|
|
|
const fileData = cacheFiles[parseInt(selectIndex)];
|
|
if (!fileData || !fileData.metadata) {
|
|
alert('Không có metadata cho cache này');
|
|
return;
|
|
}
|
|
|
|
const meta = fileData.metadata;
|
|
|
|
// Fill bbox inputs
|
|
if (meta.min_lon !== undefined) {
|
|
document.getElementById('minLon').value = meta.min_lon;
|
|
document.getElementById('maxLon').value = meta.max_lon;
|
|
document.getElementById('minLat').value = meta.min_lat;
|
|
document.getElementById('maxLat').value = meta.max_lat;
|
|
|
|
// Update map rectangle
|
|
if (rectangle) {
|
|
drawnItems.removeLayer(rectangle);
|
|
}
|
|
const bounds = L.latLngBounds(
|
|
[meta.min_lat, meta.min_lon],
|
|
[meta.max_lat, meta.max_lon]
|
|
);
|
|
rectangle = L.rectangle(bounds, {color: '#3388ff', weight: 3, fillOpacity: 0.2});
|
|
drawnItems.addLayer(rectangle);
|
|
map.fitBounds(bounds);
|
|
}
|
|
|
|
// Fill time range inputs
|
|
if (meta.start_date) {
|
|
document.getElementById('startDate').value = meta.start_date;
|
|
}
|
|
if (meta.end_date) {
|
|
document.getElementById('endDate').value = meta.end_date;
|
|
}
|
|
|
|
// Fill resolution if available
|
|
if (meta.resolution) {
|
|
document.getElementById('resolution').value = meta.resolution;
|
|
}
|
|
|
|
// Note to user
|
|
const presetNote = `📌 Đã áp dụng cache preset: ${meta.n_samples || '?'} mẫu, ${meta.start_date || '?'} → ${meta.end_date || '?'}`;
|
|
console.log(presetNote);
|
|
|
|
// Show notification
|
|
const notification = document.createElement('div');
|
|
notification.style.cssText = 'position:fixed;top:20px;right:20px;background:#28a745;color:white;padding:15px 20px;border-radius:8px;box-shadow:0 4px 6px rgba(0,0,0,0.1);z-index:10000;animation:slideIn 0.3s ease-out;';
|
|
notification.innerHTML = `<strong>✅ Cache Preset Applied</strong><br>${presetNote}`;
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.style.animation = 'slideOut 0.3s ease-out';
|
|
setTimeout(() => notification.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
// Clear cache
|
|
async function clearCache() {
|
|
if (!confirm('Bạn có chắc muốn xóa toàn bộ cache?\n\nCache giúp test nhanh hơn bằng cách lưu lại dữ liệu đã download.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/cache/clear`, {
|
|
method: 'POST'
|
|
});
|
|
const result = await response.json();
|
|
alert(result.message);
|
|
loadCacheInfo(); // Refresh info
|
|
} catch (error) {
|
|
alert('Lỗi xóa cache: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// === PROVINCE SELECTION FUNCTIONS ===
|
|
let allProvinces = {};
|
|
let allProvincesMerged = {};
|
|
let currentProvinceName = '';
|
|
let currentProvinceMode = '63'; // '63' or '32'
|
|
|
|
// Load provinces list (both 63 and 32)
|
|
async function loadProvinces() {
|
|
try {
|
|
// Load 63 provinces
|
|
const response63 = await fetch(`${API_BASE}/provinces/by-region`);
|
|
allProvinces = await response63.json();
|
|
|
|
// Load 32 merged provinces
|
|
const response32 = await fetch(`${API_BASE}/provinces-32/by-region`);
|
|
allProvincesMerged = await response32.json();
|
|
|
|
// Default to 63 provinces
|
|
populateRegionButtons();
|
|
populateProvinceSelect();
|
|
} catch (error) {
|
|
console.error('Error loading provinces:', error);
|
|
document.getElementById('provinceSelect').innerHTML = '<option value="">Lỗi tải danh sách tỉnh</option>';
|
|
}
|
|
}
|
|
|
|
// Switch between 63 and 32 province lists
|
|
function switchProvinceList(mode) {
|
|
currentProvinceMode = mode;
|
|
|
|
// Update button styles
|
|
const btn63 = document.getElementById('btn63Provinces');
|
|
const btn32 = document.getElementById('btn32Provinces');
|
|
const modeLabel = document.getElementById('provinceListMode');
|
|
|
|
if (mode === '63') {
|
|
btn63.style.background = '#667eea';
|
|
btn63.style.color = 'white';
|
|
btn32.style.background = '#f0f0f0';
|
|
btn32.style.color = '#333';
|
|
modeLabel.textContent = 'Danh sách: 63 tỉnh';
|
|
} else {
|
|
btn63.style.background = '#f0f0f0';
|
|
btn63.style.color = '#333';
|
|
btn32.style.background = '#667eea';
|
|
btn32.style.color = 'white';
|
|
modeLabel.textContent = 'Danh sách: 32 tỉnh (sau sáp nhập)';
|
|
}
|
|
|
|
// Update region filter buttons
|
|
populateRegionButtons();
|
|
|
|
// Reload province list
|
|
populateProvinceSelect();
|
|
}
|
|
|
|
// Populate province select dropdown
|
|
function populateProvinceSelect(filterRegion = 'all') {
|
|
const select = document.getElementById('provinceSelect');
|
|
select.innerHTML = '<option value="">-- Chọn tỉnh thành để tải bbox tự động --</option>';
|
|
|
|
// Choose which province list to use
|
|
const provinceData = currentProvinceMode === '63' ? allProvinces : allProvincesMerged;
|
|
|
|
// Get all regions dynamically from data
|
|
const regions = Object.keys(provinceData);
|
|
|
|
regions.forEach(region => {
|
|
if (filterRegion !== 'all' && filterRegion !== region) {
|
|
return;
|
|
}
|
|
|
|
const provinces = provinceData[region];
|
|
if (!provinces || provinces.length === 0) return;
|
|
|
|
const optgroup = document.createElement('optgroup');
|
|
optgroup.label = `${region} (${provinces.length} tỉnh)`;
|
|
|
|
provinces.forEach(province => {
|
|
const option = document.createElement('option');
|
|
option.value = province.name;
|
|
|
|
// For merged provinces, show additional info
|
|
if (currentProvinceMode === '32' && province.merged_from) {
|
|
option.textContent = `${province.name} (${province.merged_from.join(', ')})`;
|
|
} else {
|
|
option.textContent = `${province.name} - ${province.name_en || ''}`;
|
|
}
|
|
|
|
option.dataset.bbox = JSON.stringify(province.bbox);
|
|
optgroup.appendChild(option);
|
|
});
|
|
|
|
select.appendChild(optgroup);
|
|
});
|
|
}
|
|
|
|
// Handle province selection
|
|
function onProvinceSelect(event) {
|
|
const select = event.target;
|
|
const selectedOption = select.options[select.selectedIndex];
|
|
|
|
if (!selectedOption.value) {
|
|
currentProvinceName = '';
|
|
document.getElementById('provinceDisplay').textContent = '';
|
|
return;
|
|
}
|
|
|
|
const provinceName = selectedOption.value;
|
|
const bbox = JSON.parse(selectedOption.dataset.bbox);
|
|
|
|
currentProvinceName = provinceName;
|
|
|
|
// Update bbox inputs
|
|
document.getElementById('minLon').value = bbox[0];
|
|
document.getElementById('minLat').value = bbox[1];
|
|
document.getElementById('maxLon').value = bbox[2];
|
|
document.getElementById('maxLat').value = bbox[3];
|
|
|
|
// Update bbox display
|
|
document.getElementById('bboxDisplay').textContent =
|
|
`Lon: ${bbox[0]} → ${bbox[2]}, Lat: ${bbox[1]} → ${bbox[3]}`;
|
|
document.getElementById('provinceDisplay').textContent = `📍 ${provinceName}`;
|
|
|
|
// Draw rectangle on map
|
|
const bounds = [[bbox[1], bbox[0]], [bbox[3], bbox[2]]];
|
|
|
|
// Remove previous rectangle
|
|
if (currentRectangle) {
|
|
drawnItems.removeLayer(currentRectangle);
|
|
}
|
|
|
|
// Add new rectangle
|
|
currentRectangle = L.rectangle(bounds, {
|
|
color: '#667eea',
|
|
weight: 3,
|
|
fillOpacity: 0.2
|
|
});
|
|
drawnItems.addLayer(currentRectangle);
|
|
|
|
// Fit map to bounds
|
|
map.fitBounds(bounds, { padding: [50, 50] });
|
|
|
|
// Show success notification
|
|
showNotification('success', `Đã chọn tỉnh: ${provinceName}`);
|
|
}
|
|
|
|
// Populate region filter buttons
|
|
function populateRegionButtons() {
|
|
const container = document.getElementById('regionFilterContainer');
|
|
|
|
// Keep the "Tất cả" button
|
|
const allButton = container.querySelector('[data-region="all"]');
|
|
container.innerHTML = '';
|
|
container.appendChild(allButton);
|
|
|
|
// Get regions from current data
|
|
const provinceData = currentProvinceMode === '63' ? allProvinces : allProvincesMerged;
|
|
const regions = Object.keys(provinceData);
|
|
|
|
// Add button for each region
|
|
regions.forEach(region => {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'region-filter-btn';
|
|
button.dataset.region = region;
|
|
button.textContent = region;
|
|
button.style.cssText = 'padding: 8px 16px; background: #f0f0f0; color: #333; border: none; border-radius: 6px; cursor: pointer;';
|
|
container.appendChild(button);
|
|
});
|
|
|
|
// Re-setup event listeners
|
|
setupRegionFilters();
|
|
}
|
|
|
|
// Handle region filter
|
|
function setupRegionFilters() {
|
|
const filterButtons = document.querySelectorAll('.region-filter-btn');
|
|
|
|
filterButtons.forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
// Update active button style
|
|
filterButtons.forEach(b => {
|
|
b.style.background = '#f0f0f0';
|
|
b.style.color = '#333';
|
|
b.style.fontWeight = 'normal';
|
|
});
|
|
this.style.background = '#667eea';
|
|
this.style.color = 'white';
|
|
this.style.fontWeight = '600';
|
|
|
|
// Filter provinces
|
|
const region = this.dataset.region;
|
|
populateProvinceSelect(region);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Initialize map and setup event listeners when page loads
|
|
// Note: Main initialization is in the earlier DOMContentLoaded handler
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Event listeners setup
|
|
document.getElementById('provinceSelect').addEventListener('change', onProvinceSelect);
|
|
document.getElementById('trainingShapefile').addEventListener('change', function(e) {
|
|
console.log('🔄 Shapefile selection changed to:', e.target.value);
|
|
loadShapefileLabels(e.target.value);
|
|
});
|
|
setupRegionFilters();
|
|
|
|
// Add model type change listener
|
|
document.getElementById('modelType').addEventListener('change', updateModelTypeUI);
|
|
updateModelTypeUI(); // Initial update
|
|
|
|
// Add cache preset change listener
|
|
document.getElementById('cachePreset').addEventListener('change', applyCachePreset);
|
|
});
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|