2025-12-21 14:34:18 +07:00
<!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 ( 135 deg , #667eea 0 % , #764ba2 100 % );
padding : 20 px ;
min-height : 100 vh ;
}
. container {
max-width : 1400 px ;
margin : 0 auto ;
background : white ;
border-radius : 20 px ;
box-shadow : 0 20 px 60 px rgba ( 0 , 0 , 0 , 0.3 );
overflow : hidden ;
}
. header {
background : linear-gradient ( 135 deg , #667eea 0 % , #764ba2 100 % );
color : white ;
padding : 30 px ;
text-align : center ;
}
. header h1 {
font-size : 2.5 em ;
margin-bottom : 10 px ;
}
. header p {
opacity : 0.9 ;
font-size : 1.1 em ;
}
. content {
padding : 30 px ;
display : grid ;
grid-template-columns : 1 fr 1 fr ;
gap : 30 px ;
}
# map {
height : 500 px ;
border-radius : 10 px ;
box-shadow : 0 4 px 15 px rgba ( 0 , 0 , 0 , 0.1 );
}
. map-container {
grid-column : 1 / -1 ;
}
. map-instructions {
background : #e3f2fd ;
padding : 15 px ;
border-radius : 10 px ;
margin-bottom : 15 px ;
border-left : 4 px solid #2196f3 ;
}
. map-instructions h3 {
color : #1976d2 ;
margin-bottom : 8 px ;
}
. map-instructions p {
color : #555 ;
margin : 5 px 0 ;
}
. section {
margin-bottom : 30 px ;
padding : 20 px ;
background : #f8f9fa ;
border-radius : 10 px ;
}
. section h2 {
color : #667eea ;
margin-bottom : 15 px ;
font-size : 1.5 em ;
}
. form-group {
margin-bottom : 15 px ;
}
. form-group label {
display : block ;
margin-bottom : 5 px ;
color : #333 ;
font-weight : 600 ;
}
. form-group input , . form-group select {
width : 100 % ;
padding : 10 px ;
border : 2 px solid #e0e0e0 ;
border-radius : 5 px ;
font-size : 1 em ;
transition : border-color 0.3 s ;
}
. form-group input : focus , . form-group select : focus {
outline : none ;
border-color : #667eea ;
}
. form-row {
display : grid ;
grid-template-columns : repeat ( auto - fit , minmax ( 200 px , 1 fr ));
gap : 15 px ;
}
. btn {
padding : 12 px 30 px ;
border : none ;
border-radius : 5 px ;
font-size : 1 em ;
font-weight : 600 ;
cursor : pointer ;
transition : all 0.3 s ;
margin-right : 10 px ;
}
. btn-primary {
background : linear-gradient ( 135 deg , #667eea 0 % , #764ba2 100 % );
color : white ;
}
. btn-primary : hover {
transform : translateY ( -2 px );
box-shadow : 0 5 px 15 px 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 : 20 px ;
background : white ;
border-radius : 10 px ;
border-left : 5 px solid #667eea ;
margin-bottom : 20 px ;
}
. 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 : 30 px ;
background : #e0e0e0 ;
border-radius : 15 px ;
overflow : hidden ;
margin : 10 px 0 ;
}
. progress-bar {
height : 100 % ;
background : linear-gradient ( 90 deg , #667eea 0 % , #764ba2 100 % );
transition : width 0.3 s ;
display : flex ;
align-items : center ;
justify-content : center ;
color : white ;
font-weight : 600 ;
}
. preset-buttons {
display : flex ;
gap : 10 px ;
flex-wrap : wrap ;
margin-bottom : 15 px ;
}
. preset-btn {
padding : 8 px 15 px ;
background : white ;
border : 2 px solid #667eea ;
color : #667eea ;
border-radius : 20 px ;
cursor : pointer ;
transition : all 0.3 s ;
}
. preset-btn : hover {
background : #667eea ;
color : white ;
}
. info {
background : #e3f2fd ;
padding : 15 px ;
border-radius : 5 px ;
margin-bottom : 15 px ;
border-left : 4 px solid #2196f3 ;
}
. model-list {
max-height : 400 px ;
overflow-y : auto ;
}
. model-item {
background : white ;
padding : 15 px ;
margin-bottom : 10 px ;
border-radius : 5 px ;
border : 1 px solid #e0e0e0 ;
}
. model-item h3 {
color : #667eea ;
margin-bottom : 5 px ;
}
. model-item p {
margin : 5 px 0 ;
color : #666 ;
}
/* Animations for notifications */
@ keyframes slideIn {
from {
transform : translateX ( 400 px );
opacity : 0 ;
}
to {
transform : translateX ( 0 );
opacity : 1 ;
}
}
@ keyframes slideOut {
from {
transform : translateX ( 0 );
opacity : 1 ;
}
to {
transform : translateX ( 400 px );
opacity : 0 ;
}
}
</ style >
</ head >
< body >
< div class = "container" >
< div class = "header" >
2025-12-21 17:31:51 +07:00
< h1 > Training Interface</ h1 >
2025-12-21 14:34:18 +07:00
< p > Giao diện training model phân loại đất từ ảnh vệ tinh</ p >
</ div >
2025-12-22 07:20:41 +07:00
< 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 >
2025-12-22 15:43:23 +07:00
< a href = "/reports" style = "padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;" > 📝 Reports</ a >
2025-12-22 07:20:41 +07:00
</ div >
2025-12-21 14:34:18 +07:00
< 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 >
2026-01-03 22:35:33 +07:00
<!-- 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 >
2025-12-21 14:34:18 +07:00
< div class = "map-container" >
< div class = "map-instructions" >
2026-01-03 22:35:33 +07:00
< 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
2025-12-21 14:34:18 +07:00
< span style = "display: inline-block; width: 24px; height: 24px; background: white; border: 2px solid #333; vertical-align: middle; margin: 0 5px;" > □</ span >
2026-01-03 22:35:33 +07:00
ở góc trên bên trái của bản đồ để vẽ khu vực tùy chỉnh
2025-12-21 14:34:18 +07:00
</ 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 >
2026-01-03 22:35:33 +07:00
< span id = "provinceDisplay" style = "margin-left: 10px; color: #667eea; font-weight: 600;" ></ span >
2025-12-21 14:34:18 +07:00
</ div >
</ div >
<!-- Hidden inputs to store bbox values -->
2026-01-05 11:19:34 +07:00
< 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 >
2025-12-21 14:34:18 +07:00
< 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 >
2026-01-05 11:19:34 +07:00
< input type = "date" id = "endDate" value = "2023-12-31" required >
2025-12-21 14:34:18 +07:00
</ div >
</ div >
2026-01-05 11:19:34 +07:00
< 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 >
2025-12-21 14:34:18 +07:00
< 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 >
2026-01-03 22:35:33 +07:00
< option value = "swin-unet" > 🌟 Swin-UNet (Transformer + U-Net, Độ chính xác cao, Hỗ trợ GPU)</ option >
2026-01-06 12:25:42 +07:00
< option value = "mobilenet-lraspp" > 📱 MobileNetV3 + LR-ASPP (Nhẹ, Nhanh, Semantic Segmentation, Hỗ trợ GPU)</ option >
2025-12-21 14:34:18 +07:00
</ 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 >
2026-01-03 22:35:33 +07:00
< input type = "number" step = "0.0001" id = "learningRate" value = "0.1" min = "0.0001" max = "1" required >
2025-12-21 14:34:18 +07:00
</ 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 >
2025-12-21 17:31:51 +07:00
< 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 >
2025-12-21 14:34:18 +07:00
</ 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 >
2025-12-21 17:31:51 +07:00
2025-12-21 14:34:18 +07:00
</ 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 : []
};
2026-01-05 11:19:34 +07:00
let trainingFiles = []; // Store training shapefile data
let currentShapefileData = null ; // Currently selected shapefile data
2025-12-21 14:34:18 +07:00
2025-12-21 17:31:51 +07:00
// Load presets and models only after DOM is ready
document . addEventListener ( 'DOMContentLoaded' , async () => {
2026-01-05 11:19:34 +07:00
// Initialize map first - IMPORTANT!
initMap ();
loadCacheInfo ();
loadProvinces ();
// Then load other data
2025-12-21 14:34:18 +07:00
await loadPresets ();
await loadModels ();
await loadReports ();
await loadSystemInfo ();
2026-01-05 11:19:34 +07:00
await loadTrainingFiles (); // Load training shapefiles - map must be ready
2025-12-21 14:34:18 +07:00
checkStatus ();
loadTrainingHistory ();
2025-12-21 17:31:51 +07:00
});
2025-12-21 14:34:18 +07:00
2026-01-05 11:19:34 +07:00
// 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 );
}
2025-12-21 14:34:18 +07:00
// 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 ();
2026-01-05 11:19:34 +07:00
// Get selected training shapefile
const selectedShapefile = document . getElementById ( 'trainingShapefile' ). value ;
const trainingShapefile = selectedShapefile || 'train/ST_training data_updated_1130points_new.shp' ;
2025-12-21 14:34:18 +07:00
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' ,
2026-01-05 11:19:34 +07:00
use_cache : document . getElementById ( 'useCache' ). checked ,
training_shapefile : trainingShapefile
2025-12-21 14:34:18 +07:00
};
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>
2026-01-03 22:35:33 +07:00
${ 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>` : '' }
2025-12-21 14:34:18 +07:00
` ;
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' ,
2026-01-03 22:35:33 +07:00
'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' ,
2026-01-06 12:25:42 +07:00
'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'
2025-12-21 14:34:18 +07:00
};
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
2026-01-03 22:35:33 +07:00
} 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
2026-01-06 12:25:42 +07:00
} 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
2025-12-21 14:34:18 +07:00
}
2026-01-06 12:25:42 +07:00
// 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' ) {
2025-12-21 14:34:18 +07:00
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 );
}
}
2026-01-03 22:35:33 +07:00
// === 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 );
});
});
}
2026-01-05 11:19:34 +07:00
// Initialize map and setup event listeners when page loads
// Note: Main initialization is in the earlier DOMContentLoaded handler
2025-12-21 14:34:18 +07:00
document . addEventListener ( 'DOMContentLoaded' , function () {
2026-01-05 11:19:34 +07:00
// Event listeners setup
2026-01-03 22:35:33 +07:00
document . getElementById ( 'provinceSelect' ). addEventListener ( 'change' , onProvinceSelect );
2026-01-05 11:19:34 +07:00
document . getElementById ( 'trainingShapefile' ). addEventListener ( 'change' , function ( e ) {
console . log ( '🔄 Shapefile selection changed to:' , e . target . value );
loadShapefileLabels ( e . target . value );
});
2026-01-03 22:35:33 +07:00
setupRegionFilters ();
2025-12-21 14:34:18 +07:00
// 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 >