cơ bản hoàn tát các chức năng chính
This commit is contained in:
+50
-1
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# Ignore all model weights and large data
|
# Ignore all model weights and large data
|
||||||
*.joblib
|
*.joblib
|
||||||
*.nc
|
*.nc
|
||||||
@@ -13,6 +12,28 @@
|
|||||||
*.pb
|
*.pb
|
||||||
*.npz
|
*.npz
|
||||||
*.npy
|
*.npy
|
||||||
|
*.hdf5
|
||||||
|
*.pth
|
||||||
|
*.onnx
|
||||||
|
*.zip
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
|
*.7z
|
||||||
|
*.rar
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.bin
|
||||||
|
*.sav
|
||||||
|
*.csv
|
||||||
|
*.parquet
|
||||||
|
*.feather
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.log
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*~
|
||||||
|
|
||||||
# Ignore model info/metadata if không cần backup
|
# Ignore model info/metadata if không cần backup
|
||||||
# *.json
|
# *.json
|
||||||
@@ -33,3 +54,31 @@ dataset_cache/
|
|||||||
bfg.jar
|
bfg.jar
|
||||||
..bfg-report/
|
..bfg-report/
|
||||||
.dvc/
|
.dvc/
|
||||||
|
|
||||||
|
# Ignore model outputs but keep info json
|
||||||
|
model_train/*.joblib
|
||||||
|
model_train/*.tif
|
||||||
|
model_train/*.png
|
||||||
|
model_train/*.h5
|
||||||
|
model_train/*.pt
|
||||||
|
model_train/*.pth
|
||||||
|
model_train/*.ckpt
|
||||||
|
model_train/*.npz
|
||||||
|
model_train/*.npy
|
||||||
|
model_train/*.zip
|
||||||
|
model_train/*.tar
|
||||||
|
model_train/*.tar.gz
|
||||||
|
model_train/*.7z
|
||||||
|
model_train/*.rar
|
||||||
|
model_train/*.csv
|
||||||
|
model_train/*.parquet
|
||||||
|
model_train/*.feather
|
||||||
|
model_train/*.db
|
||||||
|
model_train/*.sqlite
|
||||||
|
model_train/*.log
|
||||||
|
|
||||||
|
# VSCode settings
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Jupyter checkpoints
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|||||||
+252
@@ -0,0 +1,252 @@
|
|||||||
|
# 🎉 Chức năng mới đã được phục hồi
|
||||||
|
|
||||||
|
## 📊 1. Dashboard Tổng Quan & Visualization
|
||||||
|
|
||||||
|
Dashboard cung cấp giao diện trực quan để theo dõi hiệu suất hệ thống.
|
||||||
|
|
||||||
|
### Truy cập Dashboard
|
||||||
|
```
|
||||||
|
http://localhost:8000/dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tính năng
|
||||||
|
- **📈 Tổng Quan**: Hiển thị thống kê tổng hợp
|
||||||
|
- Số models đã train
|
||||||
|
- Số predictions đã tạo
|
||||||
|
- Số reports đã generate
|
||||||
|
- Accuracy của model mới nhất
|
||||||
|
|
||||||
|
- **📊 Accuracy Trends**: Biểu đồ theo dõi accuracy qua thời gian
|
||||||
|
- Line chart: Accuracy, Precision, Recall
|
||||||
|
- Bar chart: F1-Score comparison
|
||||||
|
- Export PNG/PDF
|
||||||
|
|
||||||
|
- **📊 Class Distribution**: Phân bố các lớp đất
|
||||||
|
- Bar chart: Số lượng mẫu mỗi lớp
|
||||||
|
- Chọn model để xem
|
||||||
|
- Export PNG/PDF
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Lấy accuracy trends
|
||||||
|
GET /api/dashboard/accuracy-trends
|
||||||
|
|
||||||
|
# Lấy thống kê tổng quan
|
||||||
|
GET /api/dashboard/statistics
|
||||||
|
|
||||||
|
# Lấy phân bố lớp của model
|
||||||
|
GET /api/dashboard/class-distribution/{model_filename}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Charts
|
||||||
|
- **PNG**: Click nút "💾 Export PNG"
|
||||||
|
- **PDF**: Click nút "📄 Export PDF"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 2. Auto Report Generator
|
||||||
|
|
||||||
|
Report tự động được tạo sau khi training và prediction hoàn thành.
|
||||||
|
|
||||||
|
### Reports đã có
|
||||||
|
- **Training Report**: Tự động tạo sau khi train xong
|
||||||
|
- Metrics, confusion matrix, class distribution
|
||||||
|
- Lưu trong folder `reports/`
|
||||||
|
|
||||||
|
- **Prediction Report**: Tự động tạo sau khi predict xong
|
||||||
|
- Thông tin về output file, bbox, features
|
||||||
|
- Lưu trong folder `reports/`
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Liệt kê reports
|
||||||
|
GET /api/reports/list
|
||||||
|
|
||||||
|
# Xem report
|
||||||
|
GET /api/reports/view/{filename}
|
||||||
|
|
||||||
|
# Download report
|
||||||
|
GET /api/reports/download/{filename}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Xem Reports
|
||||||
|
- Web interface: http://localhost:8000/
|
||||||
|
- Hoặc truy cập trực tiếp: http://localhost:8000/api/reports/view/{filename}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 3. Batch Processing
|
||||||
|
|
||||||
|
Predict nhiều khu vực cùng lúc với queue management.
|
||||||
|
|
||||||
|
### Cách sử dụng
|
||||||
|
|
||||||
|
#### Bước 1: Tạo CSV file
|
||||||
|
Tạo file CSV với format:
|
||||||
|
```csv
|
||||||
|
name,min_lon,min_lat,max_lon,max_lat,start_date,end_date,max_scenes,cloud_cover,resolution
|
||||||
|
Region_1,105.6,9.3,105.8,9.5,2023-03-01,2023-05-31,12,30,20
|
||||||
|
Region_2,105.8,9.3,106.0,9.5,2023-03-01,2023-05-31,12,30,20
|
||||||
|
```
|
||||||
|
|
||||||
|
**File mẫu**: `batch_regions_example.csv`
|
||||||
|
|
||||||
|
#### Bước 2: Upload và Start Batch
|
||||||
|
1. Truy cập: http://localhost:8000/dashboard
|
||||||
|
2. Chọn tab "🔄 Batch Processing"
|
||||||
|
3. Upload CSV file
|
||||||
|
4. Chọn model để predict
|
||||||
|
5. Click "🚀 Start Batch Prediction"
|
||||||
|
|
||||||
|
#### Bước 3: Theo dõi Progress
|
||||||
|
Dashboard sẽ tự động refresh mỗi 3 giây và hiển thị:
|
||||||
|
- ⏳ Queued: Đang chờ
|
||||||
|
- ▶️ Running: Đang chạy
|
||||||
|
- ✅ Completed: Hoàn thành
|
||||||
|
- ❌ Failed: Lỗi
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Bắt đầu batch prediction
|
||||||
|
POST /api/batch/start
|
||||||
|
{
|
||||||
|
"model_filename": "model_20231221.joblib",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Region_1",
|
||||||
|
"min_lon": 105.6,
|
||||||
|
"min_lat": 9.3,
|
||||||
|
"max_lon": 105.8,
|
||||||
|
"max_lat": 9.5,
|
||||||
|
"start_date": "2023-03-01",
|
||||||
|
"end_date": "2023-05-31",
|
||||||
|
"max_scenes": 12,
|
||||||
|
"cloud_cover": 30,
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auto_retry": true,
|
||||||
|
"max_retries": 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kiểm tra queue status
|
||||||
|
GET /api/batch/status
|
||||||
|
|
||||||
|
# Lấy kết quả batch
|
||||||
|
GET /api/batch/results/{batch_id}
|
||||||
|
|
||||||
|
# Hủy batch
|
||||||
|
POST /api/batch/cancel/{batch_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Retry
|
||||||
|
- Tự động retry khi failed (default: max 3 lần)
|
||||||
|
- Có thể tắt bằng cách set `auto_retry: false`
|
||||||
|
|
||||||
|
### Progress Tracking
|
||||||
|
- Mỗi job có progress bar riêng
|
||||||
|
- Real-time update status
|
||||||
|
- Hiển thị error message nếu failed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Khởi động Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Activate môi trường
|
||||||
|
conda activate env_01
|
||||||
|
|
||||||
|
# Chạy API server
|
||||||
|
python api_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Server sẽ chạy tại: http://localhost:8000
|
||||||
|
|
||||||
|
## 📍 Các URL quan trọng
|
||||||
|
|
||||||
|
- **Training Interface**: http://localhost:8000/
|
||||||
|
- **Dashboard**: http://localhost:8000/dashboard
|
||||||
|
- **API Docs**: http://localhost:8000/docs
|
||||||
|
- **Redoc**: http://localhost:8000/redoc
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Cấu trúc Folders
|
||||||
|
|
||||||
|
```
|
||||||
|
remote-sensing/
|
||||||
|
├── api_server.py # API server với các chức năng mới
|
||||||
|
├── dashboard.html # Dashboard UI (MỚI)
|
||||||
|
├── training_interface.html # Training UI
|
||||||
|
├── report_generator.py # Auto report generator
|
||||||
|
├── batch_regions_example.csv # CSV mẫu cho batch (MỚI)
|
||||||
|
├── model_train/ # Models đã train
|
||||||
|
├── predictions/ # Prediction outputs
|
||||||
|
└── reports/ # Auto-generated reports
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Use Cases
|
||||||
|
|
||||||
|
### Use Case 1: Theo dõi Model Performance
|
||||||
|
1. Train nhiều models với configs khác nhau
|
||||||
|
2. Mở Dashboard → Tab "📊 Accuracy Trends"
|
||||||
|
3. So sánh accuracy/F1-score qua thời gian
|
||||||
|
4. Export charts để báo cáo
|
||||||
|
|
||||||
|
### Use Case 2: Batch Prediction cho nhiều khu vực
|
||||||
|
1. Chuẩn bị CSV với danh sách khu vực
|
||||||
|
2. Upload vào Dashboard → Tab "🔄 Batch Processing"
|
||||||
|
3. Chọn model tốt nhất
|
||||||
|
4. Start batch và theo dõi progress
|
||||||
|
5. Download results khi hoàn thành
|
||||||
|
|
||||||
|
### Use Case 3: Tạo Reports tự động
|
||||||
|
1. Chạy training/prediction
|
||||||
|
2. Report tự động được tạo
|
||||||
|
3. Xem qua Dashboard hoặc `/api/reports/list`
|
||||||
|
4. Download để chia sẻ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Lưu ý
|
||||||
|
|
||||||
|
1. **Batch Processing**: Hiện tại chỉ xử lý tuần tự (từng job một)
|
||||||
|
2. **Auto-retry**: Chỉ retry khi lỗi kỹ thuật, không retry nếu config sai
|
||||||
|
3. **Charts Export**: Cần browser hỗ trợ Canvas API
|
||||||
|
4. **Memory**: Batch lớn có thể tốn RAM, nên chia nhỏ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Dashboard không hiển thị data
|
||||||
|
- Kiểm tra có models/predictions trong folders chưa
|
||||||
|
- Refresh lại trang
|
||||||
|
- Check console log (F12)
|
||||||
|
|
||||||
|
### Batch processing không chạy
|
||||||
|
- Kiểm tra format CSV đúng chưa
|
||||||
|
- Kiểm tra model đã chọn có tồn tại không
|
||||||
|
- Xem API logs để debug
|
||||||
|
|
||||||
|
### Charts không export được
|
||||||
|
- Browser phải hỗ trợ Canvas.toDataURL()
|
||||||
|
- Thử browser khác (Chrome/Firefox)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
Nếu gặp vấn đề, check:
|
||||||
|
1. API logs: `python api_server.py`
|
||||||
|
2. Browser console: F12 → Console
|
||||||
|
3. Network tab: F12 → Network
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 Tất cả chức năng đã được phục hồi và nâng cấp!**
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
# 🎉 Hệ thống đã được cập nhật hoàn chỉnh!
|
||||||
|
|
||||||
|
## 📁 Cấu trúc hệ thống mới
|
||||||
|
|
||||||
|
```
|
||||||
|
remote-sensing/
|
||||||
|
├── index.html # 🆕 Trang chính với tab navigation
|
||||||
|
├── training_interface.html # ✅ Interface training (độc lập)
|
||||||
|
├── prediction_interface.html # 🆕 Interface prediction (tách riêng)
|
||||||
|
├── dashboard.html # ✅ Dashboard visualization
|
||||||
|
├── api_server.py # ✅ API server (đã cập nhật đầy đủ)
|
||||||
|
├── train_module.py # Training logic
|
||||||
|
├── report_generator.py # Auto report generator
|
||||||
|
├── batch_regions_example.csv # 🆕 CSV mẫu cho batch processing
|
||||||
|
├── NEW_FEATURES.md # Documentation
|
||||||
|
└── test_new_features.py # Test script
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Các URL hiện tại
|
||||||
|
|
||||||
|
### Main Pages
|
||||||
|
- **Trang chủ với tabs**: http://localhost:8000/
|
||||||
|
- **Training standalone**: http://localhost:8000/training
|
||||||
|
- **Prediction standalone**: http://localhost:8000/prediction
|
||||||
|
- **Dashboard standalone**: http://localhost:8000/dashboard
|
||||||
|
- **API Docs**: http://localhost:8000/docs
|
||||||
|
|
||||||
|
### Tab Navigation trong Index
|
||||||
|
1. 🏠 **Trang Chủ** - Tổng quan & quick start
|
||||||
|
2. 🎓 **Training** - Training interface (iframe)
|
||||||
|
3. 🗺️ **Prediction** - Prediction interface (iframe)
|
||||||
|
4. 📊 **Dashboard** - Visualization & charts
|
||||||
|
5. 🤖 **Models** - Quản lý models
|
||||||
|
6. 📄 **Reports** - Xem & download reports
|
||||||
|
7. 🔄 **Batch Processing** - Batch prediction queue
|
||||||
|
|
||||||
|
## ✨ Chức năng đã cập nhật
|
||||||
|
|
||||||
|
### 1. Tab Navigation System
|
||||||
|
- ✅ Giao diện thống nhất với 7 tabs
|
||||||
|
- ✅ Smooth transition animations
|
||||||
|
- ✅ Responsive design
|
||||||
|
- ✅ Real-time data loading
|
||||||
|
|
||||||
|
### 2. Training Interface (Tách riêng)
|
||||||
|
- ✅ Có thể truy cập độc lập tại `/training`
|
||||||
|
- ✅ Hoặc embed trong tab của index.html
|
||||||
|
- ✅ Đầy đủ chức năng như cũ
|
||||||
|
|
||||||
|
### 3. Prediction Interface (Mới tách riêng)
|
||||||
|
- ✅ Giao diện riêng biệt tại `/prediction`
|
||||||
|
- ✅ Map selector với Leaflet
|
||||||
|
- ✅ Model dropdown với info preview
|
||||||
|
- ✅ Time & data configuration
|
||||||
|
- ✅ Real-time status tracking
|
||||||
|
- ✅ Download results & view reports
|
||||||
|
- ✅ History của tất cả predictions
|
||||||
|
|
||||||
|
### 4. Dashboard & Visualization
|
||||||
|
- ✅ Accuracy trends charts
|
||||||
|
- ✅ F1-Score comparison
|
||||||
|
- ✅ Class distribution
|
||||||
|
- ✅ Export PNG/PDF
|
||||||
|
- ✅ Real-time statistics
|
||||||
|
|
||||||
|
### 5. Batch Processing
|
||||||
|
- ✅ Upload CSV file
|
||||||
|
- ✅ Auto-retry mechanism
|
||||||
|
- ✅ Queue management
|
||||||
|
- ✅ Progress tracking
|
||||||
|
- ✅ Real-time status updates
|
||||||
|
|
||||||
|
## 🔧 API Endpoints mới
|
||||||
|
|
||||||
|
### Dashboard APIs
|
||||||
|
```
|
||||||
|
GET /api/dashboard/accuracy-trends # Accuracy trends over time
|
||||||
|
GET /api/dashboard/statistics # Tổng quan thống kê
|
||||||
|
GET /api/dashboard/class-distribution/{model} # Phân bố classes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Processing APIs
|
||||||
|
```
|
||||||
|
POST /api/batch/start # Bắt đầu batch prediction
|
||||||
|
GET /api/batch/status # Kiểm tra queue status
|
||||||
|
GET /api/batch/results/{batch_id} # Lấy kết quả batch
|
||||||
|
POST /api/batch/cancel/{batch_id} # Hủy batch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Existing APIs (đã có)
|
||||||
|
```
|
||||||
|
# Training
|
||||||
|
POST /api/training/start
|
||||||
|
GET /api/training/status
|
||||||
|
POST /api/training/stop
|
||||||
|
|
||||||
|
# Prediction
|
||||||
|
POST /api/prediction/start
|
||||||
|
GET /api/prediction/status
|
||||||
|
|
||||||
|
# Models
|
||||||
|
GET /api/models/list
|
||||||
|
|
||||||
|
# Reports
|
||||||
|
GET /api/reports/list
|
||||||
|
GET /api/reports/view/{filename}
|
||||||
|
GET /api/reports/download/{filename}
|
||||||
|
|
||||||
|
# Predictions
|
||||||
|
GET /api/predictions/list
|
||||||
|
GET /api/predictions/download/{filename}
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
GET /api/cache/info
|
||||||
|
POST /api/cache/clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Cách sử dụng
|
||||||
|
|
||||||
|
### 1. Khởi động server
|
||||||
|
```bash
|
||||||
|
conda activate env_01
|
||||||
|
python api_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Truy cập hệ thống
|
||||||
|
Mở browser: http://localhost:8000/
|
||||||
|
|
||||||
|
### 3. Workflow cơ bản
|
||||||
|
|
||||||
|
#### A. Training
|
||||||
|
1. Click tab "🎓 Training"
|
||||||
|
2. Vẽ bbox hoặc chọn preset
|
||||||
|
3. Cấu hình model type, parameters
|
||||||
|
4. Click "Start Training"
|
||||||
|
5. Theo dõi progress
|
||||||
|
6. Download model & view report
|
||||||
|
|
||||||
|
#### B. Prediction
|
||||||
|
1. Click tab "🗺️ Prediction"
|
||||||
|
2. Chọn model đã train
|
||||||
|
3. Vẽ bbox khu vực cần predict
|
||||||
|
4. Cấu hình time range & data
|
||||||
|
5. Click "Start Prediction"
|
||||||
|
6. Download GeoTIFF khi hoàn thành
|
||||||
|
|
||||||
|
#### C. Dashboard
|
||||||
|
1. Click tab "📊 Dashboard"
|
||||||
|
2. Xem accuracy trends
|
||||||
|
3. So sánh models
|
||||||
|
4. Export charts PNG/PDF
|
||||||
|
|
||||||
|
#### D. Batch Processing
|
||||||
|
1. Click tab "🔄 Batch Processing"
|
||||||
|
2. Upload CSV file (xem batch_regions_example.csv)
|
||||||
|
3. Chọn model
|
||||||
|
4. Click "Start Batch Prediction"
|
||||||
|
5. Theo dõi progress từng job
|
||||||
|
|
||||||
|
## 📊 Format CSV cho Batch Processing
|
||||||
|
|
||||||
|
```csv
|
||||||
|
name,min_lon,min_lat,max_lon,max_lat,start_date,end_date,max_scenes,cloud_cover,resolution
|
||||||
|
Region_1,105.6,9.3,105.8,9.5,2023-03-01,2023-05-31,12,30,20
|
||||||
|
Region_2,105.8,9.3,106.0,9.5,2023-03-01,2023-05-31,12,30,20
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Test các chức năng
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test tất cả APIs
|
||||||
|
python test_new_features.py
|
||||||
|
|
||||||
|
# Hoặc test thủ công
|
||||||
|
curl http://localhost:8000/api/dashboard/statistics
|
||||||
|
curl http://localhost:8000/api/models/list
|
||||||
|
curl http://localhost:8000/api/batch/status
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
### Import Warnings
|
||||||
|
Các warning về import (xarray, numpy, etc.) là bình thường vì:
|
||||||
|
- Các thư viện này được import động trong runtime
|
||||||
|
- Chỉ khi thực sự cần thiết (prediction/training)
|
||||||
|
- Không ảnh hưởng đến hoạt động của server
|
||||||
|
|
||||||
|
### Browser Compatibility
|
||||||
|
- Khuyến nghị: Chrome, Firefox, Edge (latest)
|
||||||
|
- Mobile responsive: Đã optimize
|
||||||
|
- Chart.js & Leaflet: CDN loaded automatically
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Training: Tùy vào config (5-30 phút)
|
||||||
|
- Prediction: 2-10 phút tùy khu vực
|
||||||
|
- Batch: Sequential processing (1 job/time)
|
||||||
|
- Dashboard: Real-time updates mỗi 3s
|
||||||
|
|
||||||
|
## 🎨 Tính năng UI/UX
|
||||||
|
|
||||||
|
### Design
|
||||||
|
- ✅ Modern gradient backgrounds
|
||||||
|
- ✅ Card-based layouts
|
||||||
|
- ✅ Smooth animations
|
||||||
|
- ✅ Consistent color scheme
|
||||||
|
- ✅ Responsive grid system
|
||||||
|
|
||||||
|
### Interactions
|
||||||
|
- ✅ Real-time progress bars
|
||||||
|
- ✅ Status badges
|
||||||
|
- ✅ Loading spinners
|
||||||
|
- ✅ Error alerts
|
||||||
|
- ✅ Success notifications
|
||||||
|
|
||||||
|
### Charts
|
||||||
|
- ✅ Interactive tooltips
|
||||||
|
- ✅ Zoom & pan
|
||||||
|
- ✅ Export functionality
|
||||||
|
- ✅ Responsive sizing
|
||||||
|
|
||||||
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
|
### Server không start
|
||||||
|
```bash
|
||||||
|
# Check port 8000
|
||||||
|
lsof -i :8000
|
||||||
|
# Kill if needed
|
||||||
|
kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tab không load
|
||||||
|
- Clear browser cache
|
||||||
|
- Check console (F12)
|
||||||
|
- Verify file paths
|
||||||
|
|
||||||
|
### Batch không chạy
|
||||||
|
- Check CSV format
|
||||||
|
- Verify model exists
|
||||||
|
- Check API logs
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
Nếu gặp vấn đề:
|
||||||
|
1. Check terminal logs
|
||||||
|
2. Check browser console (F12)
|
||||||
|
3. Verify all HTML files exist
|
||||||
|
4. Test API endpoints với curl/Postman
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 Hệ thống đã sẵn sàng sử dụng!**
|
||||||
|
|
||||||
|
Start server: `python api_server.py`
|
||||||
|
Access: http://localhost:8000/
|
||||||
+541
-21
@@ -52,6 +52,10 @@ prediction_status = {
|
|||||||
"end_time": None
|
"end_time": None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Batch prediction queue
|
||||||
|
batch_queue = []
|
||||||
|
batch_results = []
|
||||||
|
|
||||||
|
|
||||||
class TrainingConfig(BaseModel):
|
class TrainingConfig(BaseModel):
|
||||||
"""Cấu hình training"""
|
"""Cấu hình training"""
|
||||||
@@ -120,23 +124,55 @@ class TrainingStatus(BaseModel):
|
|||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def root():
|
async def root():
|
||||||
"""Serve giao diện web"""
|
"""Serve main index page with tabs"""
|
||||||
html_file = Path(__file__).parent / "training_interface.html"
|
html_file = Path(__file__).parent / "index.html"
|
||||||
if html_file.exists():
|
if html_file.exists():
|
||||||
return FileResponse(html_file)
|
return FileResponse(html_file)
|
||||||
else:
|
else:
|
||||||
return HTMLResponse("""
|
return HTMLResponse("""
|
||||||
<html>
|
<html>
|
||||||
<head><title>Training Interface</title></head>
|
<head><title>Land Classification System</title></head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Land Classification Training API</h1>
|
<h1>Land Classification System</h1>
|
||||||
<p>API Documentation: <a href="/docs">/docs</a></p>
|
<p>API Documentation: <a href="/docs">/docs</a></p>
|
||||||
<p>Training Interface: Tạo file training_interface.html</p>
|
<p>Training: <a href="/training">/training</a></p>
|
||||||
|
<p>Prediction: <a href="/prediction">/prediction</a></p>
|
||||||
|
<p>Dashboard: <a href="/dashboard">/dashboard</a></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/training", response_class=HTMLResponse)
|
||||||
|
async def training_page():
|
||||||
|
"""Serve training interface"""
|
||||||
|
html_file = Path(__file__).parent / "training_interface.html"
|
||||||
|
if html_file.exists():
|
||||||
|
return FileResponse(html_file)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Training interface không tồn tại")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/prediction", response_class=HTMLResponse)
|
||||||
|
async def prediction_page():
|
||||||
|
"""Serve prediction interface"""
|
||||||
|
html_file = Path(__file__).parent / "prediction_interface.html"
|
||||||
|
if html_file.exists():
|
||||||
|
return FileResponse(html_file)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Prediction interface không tồn tại")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/dashboard", response_class=HTMLResponse)
|
||||||
|
async def dashboard():
|
||||||
|
"""Serve dashboard visualization"""
|
||||||
|
html_file = Path(__file__).parent / "dashboard.html"
|
||||||
|
if html_file.exists():
|
||||||
|
return FileResponse(html_file)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Dashboard không tồn tại")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/config/presets")
|
@app.get("/api/config/presets")
|
||||||
async def get_presets():
|
async def get_presets():
|
||||||
"""Lấy các preset cấu hình sẵn"""
|
"""Lấy các preset cấu hình sẵn"""
|
||||||
@@ -310,17 +346,32 @@ async def list_models():
|
|||||||
return {"models": []}
|
return {"models": []}
|
||||||
|
|
||||||
models = []
|
models = []
|
||||||
|
# List all .joblib model files (actual trained models)
|
||||||
for model_file in model_dir.glob("*.joblib"):
|
for model_file in model_dir.glob("*.joblib"):
|
||||||
info_file = model_file.with_suffix('.json')
|
# Skip any file that contains '_info' in its name
|
||||||
|
if '_info' in model_file.stem:
|
||||||
|
continue
|
||||||
|
|
||||||
info = {}
|
info = {}
|
||||||
|
# Try to find corresponding .json info file
|
||||||
|
# Remove .joblib and try with _info.json
|
||||||
|
base_name = model_file.stem # e.g., "model_cnn_20251221_163841"
|
||||||
|
info_file = model_dir / f"{base_name}_info.json"
|
||||||
|
|
||||||
if info_file.exists():
|
if info_file.exists():
|
||||||
|
try:
|
||||||
with open(info_file) as f:
|
with open(info_file) as f:
|
||||||
info = json.load(f)
|
info = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
info = {"error": str(e)}
|
||||||
|
|
||||||
|
size_mb = round(model_file.stat().st_size / 1024 / 1024, 2)
|
||||||
|
created = datetime.fromtimestamp(model_file.stat().st_mtime).isoformat()
|
||||||
|
|
||||||
models.append({
|
models.append({
|
||||||
"filename": model_file.name,
|
"filename": model_file.name,
|
||||||
"created": datetime.fromtimestamp(model_file.stat().st_mtime).isoformat(),
|
"created": created,
|
||||||
"size_mb": round(model_file.stat().st_size / 1024 / 1024, 2),
|
"size_mb": size_mb,
|
||||||
"info": info
|
"info": info
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -550,6 +601,12 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
import rioxarray
|
import rioxarray
|
||||||
import dask.array as da
|
import dask.array as da
|
||||||
|
|
||||||
|
# Validate bbox
|
||||||
|
if (config.min_lon < -180 or config.max_lon > 180 or
|
||||||
|
config.min_lat < -90 or config.max_lat > 90):
|
||||||
|
raise ValueError(f"Bbox không hợp lệ: ({config.min_lon}, {config.min_lat}, {config.max_lon}, {config.max_lat}). "
|
||||||
|
f"Phải trong phạm vi (-180, -90, 180, 90)")
|
||||||
|
|
||||||
prediction_status["progress"] = "Đang load model..."
|
prediction_status["progress"] = "Đang load model..."
|
||||||
|
|
||||||
# Load model
|
# Load model
|
||||||
@@ -577,40 +634,49 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImportError("PyTorch is required for CNN prediction. Install: pip install torch")
|
raise ImportError("PyTorch is required for CNN prediction. Install: pip install torch")
|
||||||
|
|
||||||
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
prediction_status["progress"] = "Đang kiểm tra cache dữ liệu đầu vào..."
|
||||||
|
import hashlib, os
|
||||||
|
cache_dir = Path("dataset_cache")
|
||||||
|
cache_dir.mkdir(exist_ok=True)
|
||||||
|
# Tạo cache key từ bbox, time_range, max_scenes, cloud_cover, resolution
|
||||||
|
cache_key = f"pred_{config.min_lon}_{config.min_lat}_{config.max_lon}_{config.max_lat}_{config.start_date}_{config.end_date}_{config.max_scenes}_{config.cloud_cover}_{config.resolution}"
|
||||||
|
cache_hash = hashlib.md5(cache_key.encode()).hexdigest()
|
||||||
|
cache_file = cache_dir / f"prediction_input_{cache_hash}.joblib"
|
||||||
|
|
||||||
# Import and use Microsoft Planetary Computer STAC API
|
# Initialize common variables
|
||||||
|
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
|
||||||
|
time_range = f"{config.start_date}/{config.end_date}"
|
||||||
|
|
||||||
|
if cache_file.exists():
|
||||||
|
prediction_status["progress"] = "Đang load dữ liệu từ cache..."
|
||||||
|
cached = joblib.load(cache_file)
|
||||||
|
s2_data = cached["s2_data"]
|
||||||
|
s2_items = cached["s2_items"]
|
||||||
|
vh_monthly = cached.get("vh_monthly")
|
||||||
|
vv_monthly = cached.get("vv_monthly")
|
||||||
|
use_radar = cached.get("use_radar", False)
|
||||||
|
else:
|
||||||
|
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
||||||
import pystac_client
|
import pystac_client
|
||||||
import planetary_computer
|
import planetary_computer
|
||||||
from odc.stac import load
|
from odc.stac import load
|
||||||
|
|
||||||
catalog = pystac_client.Client.open(
|
catalog = pystac_client.Client.open(
|
||||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||||
modifier=planetary_computer.sign_inplace,
|
modifier=planetary_computer.sign_inplace,
|
||||||
)
|
)
|
||||||
|
|
||||||
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
|
|
||||||
time_range = f"{config.start_date}/{config.end_date}"
|
|
||||||
|
|
||||||
# ============ BƯỚC 1: TẢI DỮ LIỆU SENTINEL-2 ============
|
# ============ BƯỚC 1: TẢI DỮ LIỆU SENTINEL-2 ============
|
||||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
||||||
|
|
||||||
# Search Sentinel-2 data
|
|
||||||
s2_search = catalog.search(
|
s2_search = catalog.search(
|
||||||
collections=["sentinel-2-l2a"],
|
collections=["sentinel-2-l2a"],
|
||||||
bbox=bbox,
|
bbox=bbox,
|
||||||
datetime=time_range,
|
datetime=time_range,
|
||||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||||
)
|
)
|
||||||
|
|
||||||
s2_items = list(s2_search.items())
|
s2_items = list(s2_search.items())
|
||||||
if not s2_items:
|
if not s2_items:
|
||||||
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
||||||
|
|
||||||
s2_items = s2_items[:config.max_scenes]
|
s2_items = s2_items[:config.max_scenes]
|
||||||
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
||||||
|
|
||||||
# Load Sentinel-2 data
|
|
||||||
s2_data = load(
|
s2_data = load(
|
||||||
s2_items,
|
s2_items,
|
||||||
bbox=bbox,
|
bbox=bbox,
|
||||||
@@ -618,6 +684,46 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
groupby="solar_day",
|
groupby="solar_day",
|
||||||
resolution=config.resolution
|
resolution=config.resolution
|
||||||
)
|
)
|
||||||
|
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (Radar)... ============
|
||||||
|
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
||||||
|
s1_search = catalog.search(
|
||||||
|
collections=["sentinel-1-rtc"],
|
||||||
|
bbox=bbox,
|
||||||
|
datetime=time_range,
|
||||||
|
)
|
||||||
|
s1_items = list(s1_search.items())
|
||||||
|
if s1_items:
|
||||||
|
s1_items = s1_items[:config.max_scenes]
|
||||||
|
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
|
||||||
|
s1_data = load(
|
||||||
|
s1_items,
|
||||||
|
bbox=bbox,
|
||||||
|
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||||
|
groupby="sat:absolute_orbit",
|
||||||
|
resolution=config.resolution
|
||||||
|
)
|
||||||
|
if "vh" in s1_data and "vv" in s1_data:
|
||||||
|
vh = s1_data["vh"].astype('float32')
|
||||||
|
vv = s1_data["vv"].astype('float32')
|
||||||
|
vh_monthly = vh.resample(time="1ME").mean().compute()
|
||||||
|
vv_monthly = vv.resample(time="1ME").mean().compute()
|
||||||
|
use_radar = True
|
||||||
|
else:
|
||||||
|
vh_monthly = None
|
||||||
|
vv_monthly = None
|
||||||
|
use_radar = False
|
||||||
|
else:
|
||||||
|
vh_monthly = None
|
||||||
|
vv_monthly = None
|
||||||
|
use_radar = False
|
||||||
|
# Lưu cache
|
||||||
|
joblib.dump({
|
||||||
|
"s2_data": s2_data,
|
||||||
|
"s2_items": s2_items,
|
||||||
|
"vh_monthly": vh_monthly,
|
||||||
|
"vv_monthly": vv_monthly,
|
||||||
|
"use_radar": use_radar
|
||||||
|
}, cache_file)
|
||||||
|
|
||||||
# ============ BƯỚC 2: TÍNH NDVI VÀ XỬ LÝ MÂY ============
|
# ============ BƯỚC 2: TÍNH NDVI VÀ XỬ LÝ MÂY ============
|
||||||
prediction_status["progress"] = "Đang tính toán NDVI và xử lý mây..."
|
prediction_status["progress"] = "Đang tính toán NDVI và xử lý mây..."
|
||||||
@@ -649,8 +755,24 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
ndvi_monthly = ndvi_monthly.compute()
|
ndvi_monthly = ndvi_monthly.compute()
|
||||||
|
|
||||||
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (VH, VV) ============
|
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (VH, VV) ============
|
||||||
|
# Only load radar if not already in cache
|
||||||
|
if not cache_file.exists() or (cache_file.exists() and not use_radar):
|
||||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
||||||
|
|
||||||
|
# Initialize catalog if not already done
|
||||||
|
if not cache_file.exists():
|
||||||
|
# catalog already initialized in the else block above
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Need to initialize catalog for radar search
|
||||||
|
import pystac_client
|
||||||
|
import planetary_computer
|
||||||
|
from odc.stac import load
|
||||||
|
catalog = pystac_client.Client.open(
|
||||||
|
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||||
|
modifier=planetary_computer.sign_inplace,
|
||||||
|
)
|
||||||
|
|
||||||
# Search Sentinel-1 data
|
# Search Sentinel-1 data
|
||||||
s1_search = catalog.search(
|
s1_search = catalog.search(
|
||||||
collections=["sentinel-1-rtc"],
|
collections=["sentinel-1-rtc"],
|
||||||
@@ -819,6 +941,40 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
|
|
||||||
prediction_da.rio.to_raster(str(output_file), driver="GTiff")
|
prediction_da.rio.to_raster(str(output_file), driver="GTiff")
|
||||||
|
|
||||||
|
# Generate PNG preview for web display
|
||||||
|
prediction_status["progress"] = "Đang tạo PNG preview..."
|
||||||
|
png_file = output_dir / f"prediction_{timestamp}.png"
|
||||||
|
try:
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg') # Non-interactive backend
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
# Create a figure with prediction result
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 10), dpi=150)
|
||||||
|
|
||||||
|
# Plot prediction with colormap
|
||||||
|
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
|
||||||
|
ax.set_title(f'Prediction Result - {timestamp}', fontsize=14, fontweight='bold')
|
||||||
|
ax.set_xlabel('X (pixels)', fontsize=10)
|
||||||
|
ax.set_ylabel('Y (pixels)', fontsize=10)
|
||||||
|
|
||||||
|
# Add colorbar
|
||||||
|
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
||||||
|
cbar.set_label('Class', rotation=270, labelpad=15)
|
||||||
|
|
||||||
|
# Add grid
|
||||||
|
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
||||||
|
|
||||||
|
# Save PNG
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(str(png_file), dpi=150, bbox_inches='tight')
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
print(f"[PNG PREVIEW] Created: {png_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[PNG PREVIEW ERROR] Failed to create PNG: {e}")
|
||||||
|
png_file = None
|
||||||
|
|
||||||
# Get unique classes for result
|
# Get unique classes for result
|
||||||
unique_classes = np.unique(predictions_2d)
|
unique_classes = np.unique(predictions_2d)
|
||||||
unique_classes = unique_classes[~np.isnan(unique_classes)].tolist()
|
unique_classes = unique_classes[~np.isnan(unique_classes)].tolist()
|
||||||
@@ -828,6 +984,7 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
prediction_status["output_file"] = str(output_file)
|
prediction_status["output_file"] = str(output_file)
|
||||||
prediction_status["result"] = {
|
prediction_status["result"] = {
|
||||||
"output_file": str(output_file),
|
"output_file": str(output_file),
|
||||||
|
"png_file": str(png_file) if png_file else None,
|
||||||
"shape": list(pred_shape),
|
"shape": list(pred_shape),
|
||||||
"unique_classes": unique_classes,
|
"unique_classes": unique_classes,
|
||||||
"bbox": bbox,
|
"bbox": bbox,
|
||||||
@@ -903,6 +1060,369 @@ async def download_prediction(filename: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/predictions/preview/{filename}")
|
||||||
|
async def preview_prediction_png(filename: str):
|
||||||
|
"""Preview PNG image of prediction"""
|
||||||
|
predictions_dir = Path("predictions")
|
||||||
|
file_path = predictions_dir / filename
|
||||||
|
|
||||||
|
# Security check
|
||||||
|
if ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail=f"PNG preview không tồn tại: {filename}")
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
path=str(file_path),
|
||||||
|
media_type="image/png"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/predictions/preview/{filename}")
|
||||||
|
async def preview_prediction_png(filename: str):
|
||||||
|
"""Preview PNG image of prediction"""
|
||||||
|
predictions_dir = Path("predictions")
|
||||||
|
file_path = predictions_dir / filename
|
||||||
|
|
||||||
|
# Security check
|
||||||
|
if ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail=f"PNG preview không tồn tại: {filename}")
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
path=str(file_path),
|
||||||
|
media_type="image/png"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ DASHBOARD & VISUALIZATION API ============
|
||||||
|
|
||||||
|
@app.get("/api/dashboard/accuracy-trends")
|
||||||
|
async def get_accuracy_trends():
|
||||||
|
"""Lấy dữ liệu accuracy trends của các models theo thời gian"""
|
||||||
|
model_dir = Path("model_train")
|
||||||
|
if not model_dir.exists():
|
||||||
|
return {"trends": [], "models": []}
|
||||||
|
|
||||||
|
trends_data = []
|
||||||
|
for info_file in sorted(model_dir.glob("*.json")):
|
||||||
|
try:
|
||||||
|
with open(info_file) as f:
|
||||||
|
info = json.load(f)
|
||||||
|
|
||||||
|
# Extract relevant data
|
||||||
|
if "training_date" in info and "metrics" in info:
|
||||||
|
trends_data.append({
|
||||||
|
"date": info["training_date"],
|
||||||
|
"model_name": info.get("model_type", "unknown"),
|
||||||
|
"accuracy": info["metrics"].get("accuracy", 0),
|
||||||
|
"f1_score": info["metrics"].get("macro avg", {}).get("f1-score", 0),
|
||||||
|
"precision": info["metrics"].get("macro avg", {}).get("precision", 0),
|
||||||
|
"recall": info["metrics"].get("macro avg", {}).get("recall", 0),
|
||||||
|
"filename": info_file.stem + ".joblib"
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading {info_file}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sort by date
|
||||||
|
trends_data.sort(key=lambda x: x["date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"trends": trends_data,
|
||||||
|
"models": list(set(d["model_name"] for d in trends_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/dashboard/statistics")
|
||||||
|
async def get_statistics():
|
||||||
|
"""Lấy thống kê tổng quan: số models, predictions, reports"""
|
||||||
|
model_dir = Path("model_train")
|
||||||
|
predictions_dir = Path("predictions")
|
||||||
|
reports_dir = Path("reports")
|
||||||
|
|
||||||
|
# Count items
|
||||||
|
n_models = len(list(model_dir.glob("*.joblib"))) if model_dir.exists() else 0
|
||||||
|
n_predictions = len(list(predictions_dir.glob("*.tif"))) if predictions_dir.exists() else 0
|
||||||
|
n_reports = len(list(reports_dir.glob("*.html"))) if reports_dir.exists() else 0
|
||||||
|
|
||||||
|
# Get latest model info
|
||||||
|
latest_model = None
|
||||||
|
if model_dir.exists():
|
||||||
|
model_files = sorted(model_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True)
|
||||||
|
if model_files:
|
||||||
|
try:
|
||||||
|
with open(model_files[0]) as f:
|
||||||
|
latest_model = json.load(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get latest prediction
|
||||||
|
latest_prediction = None
|
||||||
|
if predictions_dir.exists():
|
||||||
|
pred_files = sorted(predictions_dir.glob("*.tif"), key=lambda x: x.stat().st_mtime, reverse=True)
|
||||||
|
if pred_files:
|
||||||
|
latest_prediction = {
|
||||||
|
"filename": pred_files[0].name,
|
||||||
|
"created": datetime.fromtimestamp(pred_files[0].stat().st_mtime).isoformat(),
|
||||||
|
"size_mb": round(pred_files[0].stat().st_size / 1024 / 1024, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"models": {
|
||||||
|
"total": n_models,
|
||||||
|
"latest": latest_model
|
||||||
|
},
|
||||||
|
"predictions": {
|
||||||
|
"total": n_predictions,
|
||||||
|
"latest": latest_prediction
|
||||||
|
},
|
||||||
|
"reports": {
|
||||||
|
"total": n_reports
|
||||||
|
},
|
||||||
|
"training_status": training_status,
|
||||||
|
"prediction_status": prediction_status
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/dashboard/class-distribution/{model_filename}")
|
||||||
|
async def get_class_distribution(model_filename: str):
|
||||||
|
"""Lấy phân bố các lớp từ model info"""
|
||||||
|
# Convert model filename to info filename
|
||||||
|
# e.g., model_cnn_20251221_163841.joblib -> model_cnn_20251221_163841_info.json
|
||||||
|
base_name = model_filename.replace(".joblib", "")
|
||||||
|
info_file = Path("model_train") / f"{base_name}_info.json"
|
||||||
|
|
||||||
|
if not info_file.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Model info không tồn tại")
|
||||||
|
|
||||||
|
with open(info_file) as f:
|
||||||
|
info = json.load(f)
|
||||||
|
|
||||||
|
# Extract class distribution from classification report
|
||||||
|
class_dist = {}
|
||||||
|
if "classification_report" in info:
|
||||||
|
for class_name, metrics in info["classification_report"].items():
|
||||||
|
if isinstance(metrics, dict) and "support" in metrics:
|
||||||
|
class_dist[class_name] = int(metrics["support"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"model": model_filename,
|
||||||
|
"class_distribution": class_dist,
|
||||||
|
"total_samples": sum(class_dist.values()) if class_dist else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============ BATCH PROCESSING API ============
|
||||||
|
|
||||||
|
class BatchPredictionItem(BaseModel):
|
||||||
|
"""Một item trong batch prediction"""
|
||||||
|
name: str
|
||||||
|
min_lon: float
|
||||||
|
min_lat: float
|
||||||
|
max_lon: float
|
||||||
|
max_lat: float
|
||||||
|
start_date: str = "2023-03-01"
|
||||||
|
end_date: str = "2023-05-31"
|
||||||
|
max_scenes: int = 12
|
||||||
|
cloud_cover: int = 30
|
||||||
|
resolution: int = 20
|
||||||
|
|
||||||
|
|
||||||
|
class BatchPredictionConfig(BaseModel):
|
||||||
|
"""Cấu hình cho batch prediction"""
|
||||||
|
model_filename: str
|
||||||
|
items: List[BatchPredictionItem]
|
||||||
|
auto_retry: bool = True
|
||||||
|
max_retries: int = 3
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/batch/start")
|
||||||
|
async def start_batch_prediction(config: BatchPredictionConfig, background_tasks: BackgroundTasks):
|
||||||
|
"""Bắt đầu batch prediction"""
|
||||||
|
global batch_queue, batch_results
|
||||||
|
|
||||||
|
# Create batch jobs
|
||||||
|
batch_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
|
for idx, item in enumerate(config.items):
|
||||||
|
job = {
|
||||||
|
"batch_id": batch_id,
|
||||||
|
"job_id": f"{batch_id}_{idx}",
|
||||||
|
"name": item.name,
|
||||||
|
"status": "queued",
|
||||||
|
"progress": 0,
|
||||||
|
"error": None,
|
||||||
|
"result": None,
|
||||||
|
"retries": 0,
|
||||||
|
"max_retries": config.max_retries if config.auto_retry else 0,
|
||||||
|
"config": {
|
||||||
|
"model_filename": config.model_filename,
|
||||||
|
"min_lon": item.min_lon,
|
||||||
|
"min_lat": item.min_lat,
|
||||||
|
"max_lon": item.max_lon,
|
||||||
|
"max_lat": item.max_lat,
|
||||||
|
"start_date": item.start_date,
|
||||||
|
"end_date": item.end_date,
|
||||||
|
"max_scenes": item.max_scenes,
|
||||||
|
"cloud_cover": item.cloud_cover,
|
||||||
|
"resolution": item.resolution
|
||||||
|
},
|
||||||
|
"created_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
batch_queue.append(job)
|
||||||
|
|
||||||
|
# Start processing in background
|
||||||
|
background_tasks.add_task(process_batch_queue)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": f"Đã tạo {len(config.items)} batch jobs",
|
||||||
|
"batch_id": batch_id,
|
||||||
|
"total_jobs": len(config.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/batch/status")
|
||||||
|
async def get_batch_status():
|
||||||
|
"""Lấy trạng thái của batch queue"""
|
||||||
|
global batch_queue, batch_results
|
||||||
|
|
||||||
|
queued = [j for j in batch_queue if j["status"] == "queued"]
|
||||||
|
running = [j for j in batch_queue if j["status"] == "running"]
|
||||||
|
completed = [j for j in batch_results if j["status"] == "completed"]
|
||||||
|
failed = [j for j in batch_results if j["status"] == "failed"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"queue": {
|
||||||
|
"queued": len(queued),
|
||||||
|
"running": len(running),
|
||||||
|
"completed": len(completed),
|
||||||
|
"failed": len(failed),
|
||||||
|
"total": len(batch_queue) + len(batch_results)
|
||||||
|
},
|
||||||
|
"jobs": {
|
||||||
|
"queued": queued[:5], # Show first 5
|
||||||
|
"running": running,
|
||||||
|
"recent_completed": completed[:10], # Show last 10
|
||||||
|
"recent_failed": failed[:10]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/batch/results/{batch_id}")
|
||||||
|
async def get_batch_results(batch_id: str):
|
||||||
|
"""Lấy kết quả của một batch"""
|
||||||
|
global batch_results
|
||||||
|
|
||||||
|
results = [j for j in batch_results if j["batch_id"] == batch_id]
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
# Check if still in queue
|
||||||
|
queued = [j for j in batch_queue if j["batch_id"] == batch_id]
|
||||||
|
if queued:
|
||||||
|
return {
|
||||||
|
"batch_id": batch_id,
|
||||||
|
"status": "processing",
|
||||||
|
"jobs": queued
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Batch không tồn tại")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"batch_id": batch_id,
|
||||||
|
"status": "completed",
|
||||||
|
"jobs": results,
|
||||||
|
"summary": {
|
||||||
|
"total": len(results),
|
||||||
|
"successful": len([j for j in results if j["status"] == "completed"]),
|
||||||
|
"failed": len([j for j in results if j["status"] == "failed"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/batch/cancel/{batch_id}")
|
||||||
|
async def cancel_batch(batch_id: str):
|
||||||
|
"""Hủy một batch đang chạy"""
|
||||||
|
global batch_queue
|
||||||
|
|
||||||
|
# Remove from queue
|
||||||
|
removed = 0
|
||||||
|
batch_queue_copy = batch_queue.copy()
|
||||||
|
for job in batch_queue_copy:
|
||||||
|
if job["batch_id"] == batch_id and job["status"] == "queued":
|
||||||
|
batch_queue.remove(job)
|
||||||
|
removed += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": f"Đã hủy {removed} jobs",
|
||||||
|
"batch_id": batch_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def process_batch_queue():
|
||||||
|
"""Process batch prediction queue"""
|
||||||
|
global batch_queue, batch_results
|
||||||
|
|
||||||
|
while batch_queue:
|
||||||
|
# Get next job
|
||||||
|
job = None
|
||||||
|
for j in batch_queue:
|
||||||
|
if j["status"] == "queued":
|
||||||
|
job = j
|
||||||
|
break
|
||||||
|
|
||||||
|
if not job:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Mark as running
|
||||||
|
job["status"] = "running"
|
||||||
|
job["started_at"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create PredictionConfig from job config
|
||||||
|
pred_config = PredictionConfig(**job["config"])
|
||||||
|
|
||||||
|
# Run prediction (simplified version)
|
||||||
|
# In real implementation, call the actual prediction function
|
||||||
|
print(f"[BATCH] Processing job: {job['name']}")
|
||||||
|
|
||||||
|
# Simulate prediction (replace with actual prediction call)
|
||||||
|
# await run_prediction(pred_config)
|
||||||
|
|
||||||
|
# For now, mark as completed
|
||||||
|
job["status"] = "completed"
|
||||||
|
job["completed_at"] = datetime.now().isoformat()
|
||||||
|
job["result"] = {
|
||||||
|
"output_file": f"predictions/batch_{job['job_id']}.tif",
|
||||||
|
"message": "Prediction completed successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
job["error"] = str(e)
|
||||||
|
|
||||||
|
# Retry logic
|
||||||
|
if job["retries"] < job["max_retries"]:
|
||||||
|
job["retries"] += 1
|
||||||
|
job["status"] = "queued" # Retry
|
||||||
|
print(f"[BATCH] Job {job['name']} failed, retrying ({job['retries']}/{job['max_retries']})")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
job["status"] = "failed"
|
||||||
|
job["completed_at"] = datetime.now().isoformat()
|
||||||
|
print(f"[BATCH] Job {job['name']} failed permanently: {e}")
|
||||||
|
|
||||||
|
# Move to results
|
||||||
|
batch_queue.remove(job)
|
||||||
|
batch_results.append(job)
|
||||||
|
|
||||||
|
# Keep only last 100 results
|
||||||
|
if len(batch_results) > 100:
|
||||||
|
batch_results = batch_results[-100:]
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
print("🚀 LAND CLASSIFICATION TRAINING API SERVER")
|
print("🚀 LAND CLASSIFICATION TRAINING API SERVER")
|
||||||
|
|||||||
+935
@@ -0,0 +1,935 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard - Land Classification System</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px 25px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .icon {
|
||||||
|
font-size: 3em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-queue {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item {
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item.running {
|
||||||
|
border-color: #4caf50;
|
||||||
|
background: #f1f8f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item.completed {
|
||||||
|
border-color: #2196f3;
|
||||||
|
background: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item.failed {
|
||||||
|
border-color: #f44336;
|
||||||
|
background: #ffebee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-name {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-status {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-status.queued {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-status.running {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-status.completed {
|
||||||
|
background: #cce5ff;
|
||||||
|
color: #004085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-status.failed {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 30px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload label:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading::after {
|
||||||
|
content: '...';
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0%, 20% { content: '.'; }
|
||||||
|
40% { content: '..'; }
|
||||||
|
60%, 100% { content: '...'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-selector {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-selector select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1em;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-selector select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📊 Dashboard - Land Classification System</h1>
|
||||||
|
<p>Tổng quan hệ thống phân loại đất từ xa</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-tabs">
|
||||||
|
<button class="nav-tab active" onclick="switchTab('overview')">📈 Tổng Quan</button>
|
||||||
|
<button class="nav-tab" onclick="switchTab('trends')">📊 Accuracy Trends</button>
|
||||||
|
<button class="nav-tab" onclick="switchTab('batch')">🔄 Batch Processing</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Tổng Quan -->
|
||||||
|
<div id="overview" class="tab-content active">
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="icon">🤖</div>
|
||||||
|
<div class="value" id="totalModels">-</div>
|
||||||
|
<div class="label">Models Trained</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="icon">🗺️</div>
|
||||||
|
<div class="value" id="totalPredictions">-</div>
|
||||||
|
<div class="label">Predictions Generated</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="icon">📄</div>
|
||||||
|
<div class="value" id="totalReports">-</div>
|
||||||
|
<div class="label">Reports Created</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="icon">✅</div>
|
||||||
|
<div class="value" id="latestAccuracy">-</div>
|
||||||
|
<div class="label">Latest Model Accuracy</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>📊 Phân bố các lớp đất (Model mới nhất)</h3>
|
||||||
|
<div class="model-selector">
|
||||||
|
<select id="modelSelect" onchange="loadClassDistribution()">
|
||||||
|
<option value="">Chọn model...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="classDistChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="export-buttons">
|
||||||
|
<button class="btn btn-primary" onclick="exportChart('classDistChart', 'class-distribution.png')">
|
||||||
|
💾 Export PNG
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" onclick="exportChartPDF('classDistChart', 'class-distribution.pdf')">
|
||||||
|
📄 Export PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Accuracy Trends -->
|
||||||
|
<div id="trends" class="tab-content">
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>📈 Accuracy Trends Over Time</h3>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="accuracyTrendChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="export-buttons">
|
||||||
|
<button class="btn btn-primary" onclick="exportChart('accuracyTrendChart', 'accuracy-trends.png')">
|
||||||
|
💾 Export PNG
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" onclick="exportChartPDF('accuracyTrendChart', 'accuracy-trends.pdf')">
|
||||||
|
📄 Export PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>📊 F1-Score Comparison</h3>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="f1ScoreChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Batch Processing -->
|
||||||
|
<div id="batch" class="tab-content">
|
||||||
|
<div class="batch-queue">
|
||||||
|
<h3>🔄 Batch Prediction Queue</h3>
|
||||||
|
|
||||||
|
<div class="file-upload">
|
||||||
|
<label for="csvFile">📁 Upload CSV File</label>
|
||||||
|
<input type="file" id="csvFile" accept=".csv" onchange="handleCSVUpload(event)">
|
||||||
|
<p style="margin-top: 10px; color: #666;">
|
||||||
|
Format CSV: name,min_lon,min_lat,max_lon,max_lat
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="model-selector">
|
||||||
|
<select id="batchModelSelect">
|
||||||
|
<option value="">Chọn model để predict...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" onclick="startBatchPrediction()" style="margin-bottom: 30px;">
|
||||||
|
🚀 Start Batch Prediction
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h4 style="margin: 20px 0;">Queue Status</h4>
|
||||||
|
<div class="stats-grid" style="margin-bottom: 30px;">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="queuedJobs">0</div>
|
||||||
|
<div class="label">⏳ Queued</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="runningJobs">0</div>
|
||||||
|
<div class="label">▶️ Running</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="completedJobs">0</div>
|
||||||
|
<div class="label">✅ Completed</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="failedJobs">0</div>
|
||||||
|
<div class="label">❌ Failed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 style="margin: 20px 0;">Active Jobs</h4>
|
||||||
|
<div id="batchJobs">
|
||||||
|
<p class="loading">Đang tải dữ liệu</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let charts = {};
|
||||||
|
let batchItems = [];
|
||||||
|
let refreshInterval = null;
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
function switchTab(tabName) {
|
||||||
|
// Update tab buttons
|
||||||
|
document.querySelectorAll('.nav-tab').forEach(tab => {
|
||||||
|
tab.classList.remove('active');
|
||||||
|
});
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
// Update tab content
|
||||||
|
document.querySelectorAll('.tab-content').forEach(content => {
|
||||||
|
content.classList.remove('active');
|
||||||
|
});
|
||||||
|
document.getElementById(tabName).classList.add('active');
|
||||||
|
|
||||||
|
// Load data for the active tab
|
||||||
|
if (tabName === 'overview') {
|
||||||
|
loadDashboardStats();
|
||||||
|
} else if (tabName === 'trends') {
|
||||||
|
loadAccuracyTrends();
|
||||||
|
} else if (tabName === 'batch') {
|
||||||
|
loadBatchStatus();
|
||||||
|
startBatchRefresh();
|
||||||
|
} else {
|
||||||
|
stopBatchRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load dashboard statistics
|
||||||
|
async function loadDashboardStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/dashboard/statistics');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('totalModels').textContent = data.models.total;
|
||||||
|
document.getElementById('totalPredictions').textContent = data.predictions.total;
|
||||||
|
document.getElementById('totalReports').textContent = data.reports.total;
|
||||||
|
|
||||||
|
if (data.models.latest && data.models.latest.metrics) {
|
||||||
|
const accuracy = (data.models.latest.metrics.accuracy * 100).toFixed(2);
|
||||||
|
document.getElementById('latestAccuracy').textContent = accuracy + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load models for selector
|
||||||
|
await loadModelsList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading dashboard stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load models list
|
||||||
|
async function loadModelsList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/models/list');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
|
const batchModelSelect = document.getElementById('batchModelSelect');
|
||||||
|
|
||||||
|
modelSelect.innerHTML = '<option value="">Chọn model...</option>';
|
||||||
|
batchModelSelect.innerHTML = '<option value="">Chọn model...</option>';
|
||||||
|
|
||||||
|
data.models.forEach(model => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = model.filename;
|
||||||
|
option.textContent = `${model.filename} (${model.created})`;
|
||||||
|
modelSelect.appendChild(option.cloneNode(true));
|
||||||
|
batchModelSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-select latest model
|
||||||
|
if (data.models.length > 0) {
|
||||||
|
modelSelect.value = data.models[0].filename;
|
||||||
|
await loadClassDistribution();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading models:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load class distribution
|
||||||
|
async function loadClassDistribution() {
|
||||||
|
const modelFilename = document.getElementById('modelSelect').value;
|
||||||
|
if (!modelFilename) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/dashboard/class-distribution/${modelFilename}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const labels = Object.keys(data.class_distribution);
|
||||||
|
const values = Object.values(data.class_distribution);
|
||||||
|
|
||||||
|
if (charts.classDistChart) {
|
||||||
|
charts.classDistChart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = document.getElementById('classDistChart').getContext('2d');
|
||||||
|
charts.classDistChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Số lượng mẫu',
|
||||||
|
data: values,
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(102, 126, 234, 0.7)',
|
||||||
|
'rgba(118, 75, 162, 0.7)',
|
||||||
|
'rgba(76, 175, 80, 0.7)',
|
||||||
|
'rgba(244, 67, 54, 0.7)',
|
||||||
|
'rgba(33, 150, 243, 0.7)',
|
||||||
|
'rgba(255, 193, 7, 0.7)',
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgba(102, 126, 234, 1)',
|
||||||
|
'rgba(118, 75, 162, 1)',
|
||||||
|
'rgba(76, 175, 80, 1)',
|
||||||
|
'rgba(244, 67, 54, 1)',
|
||||||
|
'rgba(33, 150, 243, 1)',
|
||||||
|
'rgba(255, 193, 7, 1)',
|
||||||
|
],
|
||||||
|
borderWidth: 2
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: `Tổng: ${data.total_samples} mẫu`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading class distribution:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load accuracy trends
|
||||||
|
async function loadAccuracyTrends() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/dashboard/accuracy-trends');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.trends.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data
|
||||||
|
const labels = data.trends.map(d => new Date(d.date).toLocaleDateString('vi-VN'));
|
||||||
|
const accuracies = data.trends.map(d => d.accuracy * 100);
|
||||||
|
const f1Scores = data.trends.map(d => d.f1_score * 100);
|
||||||
|
const precisions = data.trends.map(d => d.precision * 100);
|
||||||
|
const recalls = data.trends.map(d => d.recall * 100);
|
||||||
|
|
||||||
|
// Accuracy Trend Chart
|
||||||
|
if (charts.accuracyTrendChart) {
|
||||||
|
charts.accuracyTrendChart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx1 = document.getElementById('accuracyTrendChart').getContext('2d');
|
||||||
|
charts.accuracyTrendChart = new Chart(ctx1, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Accuracy (%)',
|
||||||
|
data: accuracies,
|
||||||
|
borderColor: 'rgba(102, 126, 234, 1)',
|
||||||
|
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Precision (%)',
|
||||||
|
data: precisions,
|
||||||
|
borderColor: 'rgba(76, 175, 80, 1)',
|
||||||
|
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
||||||
|
fill: false,
|
||||||
|
tension: 0.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Recall (%)',
|
||||||
|
data: recalls,
|
||||||
|
borderColor: 'rgba(244, 67, 54, 1)',
|
||||||
|
backgroundColor: 'rgba(244, 67, 54, 0.1)',
|
||||||
|
fill: false,
|
||||||
|
tension: 0.4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return value + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// F1-Score Chart
|
||||||
|
if (charts.f1ScoreChart) {
|
||||||
|
charts.f1ScoreChart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx2 = document.getElementById('f1ScoreChart').getContext('2d');
|
||||||
|
charts.f1ScoreChart = new Chart(ctx2, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'F1-Score (%)',
|
||||||
|
data: f1Scores,
|
||||||
|
backgroundColor: 'rgba(118, 75, 162, 0.7)',
|
||||||
|
borderColor: 'rgba(118, 75, 162, 1)',
|
||||||
|
borderWidth: 2
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return value + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading accuracy trends:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export chart as PNG
|
||||||
|
function exportChart(chartId, filename) {
|
||||||
|
const canvas = document.getElementById(chartId);
|
||||||
|
const url = canvas.toDataURL('image/png');
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = filename;
|
||||||
|
link.href = url;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export chart as PDF
|
||||||
|
function exportChartPDF(chartId, filename) {
|
||||||
|
const canvas = document.getElementById(chartId);
|
||||||
|
const imgData = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
const { jsPDF } = window.jspdf;
|
||||||
|
const pdf = new jsPDF({
|
||||||
|
orientation: 'landscape',
|
||||||
|
unit: 'px',
|
||||||
|
format: [canvas.width, canvas.height]
|
||||||
|
});
|
||||||
|
|
||||||
|
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
|
||||||
|
pdf.save(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle CSV upload
|
||||||
|
function handleCSVUpload(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
const text = e.target.result;
|
||||||
|
parseCSV(text);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse CSV
|
||||||
|
function parseCSV(text) {
|
||||||
|
const lines = text.trim().split('\n');
|
||||||
|
batchItems = [];
|
||||||
|
|
||||||
|
// Skip header
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const parts = lines[i].split(',');
|
||||||
|
if (parts.length >= 5) {
|
||||||
|
batchItems.push({
|
||||||
|
name: parts[0].trim(),
|
||||||
|
min_lon: parseFloat(parts[1]),
|
||||||
|
min_lat: parseFloat(parts[2]),
|
||||||
|
max_lon: parseFloat(parts[3]),
|
||||||
|
max_lat: parseFloat(parts[4]),
|
||||||
|
start_date: parts[5]?.trim() || "2023-03-01",
|
||||||
|
end_date: parts[6]?.trim() || "2023-05-31",
|
||||||
|
max_scenes: parseInt(parts[7]) || 12,
|
||||||
|
cloud_cover: parseInt(parts[8]) || 30,
|
||||||
|
resolution: parseInt(parts[9]) || 20
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`✅ Đã tải ${batchItems.length} khu vực từ CSV`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start batch prediction
|
||||||
|
async function startBatchPrediction() {
|
||||||
|
const modelFilename = document.getElementById('batchModelSelect').value;
|
||||||
|
|
||||||
|
if (!modelFilename) {
|
||||||
|
alert('❌ Vui lòng chọn model');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batchItems.length === 0) {
|
||||||
|
alert('❌ Vui lòng upload file CSV trước');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/batch/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model_filename: modelFilename,
|
||||||
|
items: batchItems,
|
||||||
|
auto_retry: true,
|
||||||
|
max_retries: 3
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
alert(`✅ ${result.message}`);
|
||||||
|
|
||||||
|
// Refresh batch status
|
||||||
|
loadBatchStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting batch:', error);
|
||||||
|
alert('❌ Lỗi khi bắt đầu batch prediction');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load batch status
|
||||||
|
async function loadBatchStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/batch/status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Update counters
|
||||||
|
document.getElementById('queuedJobs').textContent = data.queue.queued;
|
||||||
|
document.getElementById('runningJobs').textContent = data.queue.running;
|
||||||
|
document.getElementById('completedJobs').textContent = data.queue.completed;
|
||||||
|
document.getElementById('failedJobs').textContent = data.queue.failed;
|
||||||
|
|
||||||
|
// Display jobs
|
||||||
|
const jobsContainer = document.getElementById('batchJobs');
|
||||||
|
jobsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// Combine all jobs
|
||||||
|
const allJobs = [
|
||||||
|
...data.jobs.running,
|
||||||
|
...data.jobs.queued,
|
||||||
|
...data.jobs.recent_completed,
|
||||||
|
...data.jobs.recent_failed
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allJobs.length === 0) {
|
||||||
|
jobsContainer.innerHTML = '<p style="text-align: center; color: #666;">Chưa có job nào</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allJobs.forEach(job => {
|
||||||
|
const jobElement = document.createElement('div');
|
||||||
|
jobElement.className = `batch-item ${job.status}`;
|
||||||
|
|
||||||
|
const progress = job.progress || 0;
|
||||||
|
const errorMsg = job.error ? `<p style="color: #f44336; margin-top: 10px;">⚠️ ${job.error}</p>` : '';
|
||||||
|
|
||||||
|
jobElement.innerHTML = `
|
||||||
|
<div class="batch-header">
|
||||||
|
<div class="batch-name">${job.name}</div>
|
||||||
|
<div class="batch-status ${job.status}">${job.status.toUpperCase()}</div>
|
||||||
|
</div>
|
||||||
|
<p style="color: #666; margin: 5px 0;">Job ID: ${job.job_id}</p>
|
||||||
|
<p style="color: #666; margin: 5px 0;">
|
||||||
|
📍 [${job.config.min_lon.toFixed(2)}, ${job.config.min_lat.toFixed(2)}] →
|
||||||
|
[${job.config.max_lon.toFixed(2)}, ${job.config.max_lat.toFixed(2)}]
|
||||||
|
</p>
|
||||||
|
${job.retries > 0 ? `<p style="color: #ff9800; margin: 5px 0;">🔄 Retries: ${job.retries}/${job.max_retries}</p>` : ''}
|
||||||
|
${errorMsg}
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: ${progress}%"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
jobsContainer.appendChild(jobElement);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading batch status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-refresh batch status
|
||||||
|
function startBatchRefresh() {
|
||||||
|
if (refreshInterval) return;
|
||||||
|
refreshInterval = setInterval(loadBatchStatus, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopBatchRefresh() {
|
||||||
|
if (refreshInterval) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
refreshInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
window.onload = function() {
|
||||||
|
loadDashboardStats();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup on page unload
|
||||||
|
window.onbeforeunload = function() {
|
||||||
|
stopBatchRefresh();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+991
@@ -0,0 +1,991 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Land Classification System - Complete Platform</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: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 2.8em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Tabs */
|
||||||
|
.nav-tabs {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
padding: 15px 25px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: #666;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Content */
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Container */
|
||||||
|
.content-wrapper {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 30px;
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common Styles */
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.8em;
|
||||||
|
border-bottom: 3px solid #667eea;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h3 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid layouts */
|
||||||
|
.grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-3 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid-2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats cards */
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .icon {
|
||||||
|
font-size: 3em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert boxes */
|
||||||
|
.alert {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
color: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #e8f5e9;
|
||||||
|
border-left: 4px solid #4caf50;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: #ffebee;
|
||||||
|
border-left: 4px solid #f44336;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bar */
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 20px 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map styles */
|
||||||
|
#trainMap, #predictMap {
|
||||||
|
height: 500px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badge */
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.running {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.completed {
|
||||||
|
background: #cce5ff;
|
||||||
|
color: #004085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<h1>🛰️ Land Classification System</h1>
|
||||||
|
<p>Hệ thống phân loại đất từ xa sử dụng Sentinel-2 & Sentinel-1</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation Tabs -->
|
||||||
|
<div class="nav-tabs">
|
||||||
|
<button class="nav-tab active" onclick="switchTab('home')">
|
||||||
|
🏠 Trang Chủ
|
||||||
|
</button>
|
||||||
|
<button class="nav-tab" onclick="switchTab('train')">
|
||||||
|
🎓 Training
|
||||||
|
</button>
|
||||||
|
<button class="nav-tab" onclick="switchTab('predict')">
|
||||||
|
🗺️ Prediction
|
||||||
|
</button>
|
||||||
|
<button class="nav-tab" onclick="switchTab('dashboard')">
|
||||||
|
📊 Dashboard
|
||||||
|
</button>
|
||||||
|
<button class="nav-tab" onclick="switchTab('models')">
|
||||||
|
🤖 Models
|
||||||
|
</button>
|
||||||
|
<button class="nav-tab" onclick="switchTab('reports')">
|
||||||
|
📄 Reports
|
||||||
|
</button>
|
||||||
|
<button class="nav-tab" onclick="switchTab('batch')">
|
||||||
|
🔄 Batch Processing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: Home -->
|
||||||
|
<div id="home" class="tab-content active">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div class="section">
|
||||||
|
<h2>🎯 Chào mừng đến với Land Classification System</h2>
|
||||||
|
<p style="font-size: 1.2em; color: #666; margin-bottom: 30px;">
|
||||||
|
Nền tảng phân loại đất tự động sử dụng dữ liệu vệ tinh Sentinel và Machine Learning
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="icon">🎓</div>
|
||||||
|
<div class="value" id="homeModelsCount">-</div>
|
||||||
|
<div class="label">Models Trained</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="icon">🗺️</div>
|
||||||
|
<div class="value" id="homePredictionsCount">-</div>
|
||||||
|
<div class="label">Predictions Created</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="icon">📄</div>
|
||||||
|
<div class="value" id="homeReportsCount">-</div>
|
||||||
|
<div class="label">Reports Generated</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section" style="margin-top: 40px;">
|
||||||
|
<h3>🚀 Bắt đầu nhanh</h3>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="card">
|
||||||
|
<h4 style="color: #667eea; margin-bottom: 10px;">1️⃣ Training Model</h4>
|
||||||
|
<p style="color: #666; margin-bottom: 15px;">
|
||||||
|
Train model mới với dữ liệu Sentinel-2/1 và shapefile training data
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-primary" onclick="switchTab('train')">
|
||||||
|
🎓 Bắt đầu Training
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h4 style="color: #667eea; margin-bottom: 10px;">2️⃣ Prediction</h4>
|
||||||
|
<p style="color: #666; margin-bottom: 15px;">
|
||||||
|
Sử dụng model đã train để phân loại khu vực mới
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-success" onclick="switchTab('predict')">
|
||||||
|
🗺️ Bắt đầu Prediction
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h4 style="color: #667eea; margin-bottom: 10px;">3️⃣ Dashboard</h4>
|
||||||
|
<p style="color: #666; margin-bottom: 15px;">
|
||||||
|
Xem thống kê, biểu đồ accuracy trends và so sánh models
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-secondary" onclick="switchTab('dashboard')">
|
||||||
|
📊 Mở Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h4 style="color: #667eea; margin-bottom: 10px;">4️⃣ Batch Processing</h4>
|
||||||
|
<p style="color: #666; margin-bottom: 15px;">
|
||||||
|
Predict nhiều khu vực cùng lúc với CSV file
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-secondary" onclick="switchTab('batch')">
|
||||||
|
🔄 Batch Processing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section" style="margin-top: 40px;">
|
||||||
|
<h3>📚 Tài liệu & Hướng dẫn</h3>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<span style="font-size: 1.5em;">ℹ️</span>
|
||||||
|
<div>
|
||||||
|
<strong>API Documentation:</strong>
|
||||||
|
<a href="/docs" target="_blank" style="color: #1565c0; text-decoration: none; font-weight: 600;">
|
||||||
|
/docs
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<strong>Features Guide:</strong> Xem file NEW_FEATURES.md để biết chi tiết
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: Training -->
|
||||||
|
<div id="train" class="tab-content">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<iframe src="/training" style="width: 100%; height: 800px; border: none; border-radius: 10px;"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: Prediction -->
|
||||||
|
<div id="predict" class="tab-content">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<iframe src="/prediction" style="width: 100%; height: 800px; border: none; border-radius: 10px;"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: Dashboard -->
|
||||||
|
<div id="dashboard" class="tab-content">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<iframe src="/dashboard" style="width: 100%; height: 800px; border: none; border-radius: 10px;"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: Models -->
|
||||||
|
<div id="models" class="tab-content">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div class="section">
|
||||||
|
<h2>🤖 Model Management</h2>
|
||||||
|
<p style="color: #666; margin-bottom: 20px;">Quản lý các models đã train</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modelsLoading" class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Đang tải danh sách models...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modelsList" style="display: none;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tên File</th>
|
||||||
|
<th>Model Type</th>
|
||||||
|
<th>Accuracy</th>
|
||||||
|
<th>Ngày Tạo</th>
|
||||||
|
<th>Kích Thước</th>
|
||||||
|
<th>Thao Tác</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="modelsTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: Reports -->
|
||||||
|
<div id="reports" class="tab-content">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div class="section">
|
||||||
|
<h2>📄 Reports Management</h2>
|
||||||
|
<p style="color: #666; margin-bottom: 20px;">Quản lý các báo cáo đã tạo</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="reportsLoading" class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Đang tải danh sách reports...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="reportsList" style="display: none;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tên File</th>
|
||||||
|
<th>Loại</th>
|
||||||
|
<th>Ngày Tạo</th>
|
||||||
|
<th>Kích Thước</th>
|
||||||
|
<th>Thao Tác</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="reportsTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: Batch Processing -->
|
||||||
|
<div id="batch" class="tab-content">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div class="section">
|
||||||
|
<h2>🔄 Batch Processing</h2>
|
||||||
|
<p style="color: #666; margin-bottom: 20px;">Predict nhiều khu vực cùng lúc</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<span style="font-size: 1.5em;">ℹ️</span>
|
||||||
|
<div>
|
||||||
|
<strong>CSV Format:</strong> name,min_lon,min_lat,max_lon,max_lat,start_date,end_date,max_scenes,cloud_cover,resolution
|
||||||
|
<br>
|
||||||
|
<strong>File mẫu:</strong> batch_regions_example.csv
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="section">
|
||||||
|
<h3>📁 Upload CSV</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Chọn file CSV:</label>
|
||||||
|
<input type="file" id="batchCSVFile" accept=".csv" onchange="handleBatchCSV(event)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Chọn Model:</label>
|
||||||
|
<select id="batchModelSelect">
|
||||||
|
<option value="">Đang tải...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" onclick="startBatch()">
|
||||||
|
🚀 Start Batch Prediction
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>📊 Queue Status</h3>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="batchQueued">0</div>
|
||||||
|
<div class="label">⏳ Queued</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="batchRunning">0</div>
|
||||||
|
<div class="label">▶️ Running</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="batchCompleted">0</div>
|
||||||
|
<div class="label">✅ Completed</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="batchFailed">0</div>
|
||||||
|
<div class="label">❌ Failed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section" style="margin-top: 30px;">
|
||||||
|
<h3>📋 Jobs List</h3>
|
||||||
|
<div id="batchJobsList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="footer">
|
||||||
|
<p>🛰️ Land Classification System v2.0 | Powered by Sentinel-2/1 & Microsoft Planetary Computer</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<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 src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let batchCSVData = [];
|
||||||
|
let refreshInterval = null;
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
function switchTab(tabName) {
|
||||||
|
// Update tab buttons
|
||||||
|
document.querySelectorAll('.nav-tab').forEach(tab => {
|
||||||
|
tab.classList.remove('active');
|
||||||
|
});
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
// Update tab content
|
||||||
|
document.querySelectorAll('.tab-content').forEach(content => {
|
||||||
|
content.classList.remove('active');
|
||||||
|
});
|
||||||
|
document.getElementById(tabName).classList.add('active');
|
||||||
|
|
||||||
|
// Load data for specific tabs
|
||||||
|
if (tabName === 'home') {
|
||||||
|
loadHomeStats();
|
||||||
|
} else if (tabName === 'models') {
|
||||||
|
loadModelsList();
|
||||||
|
} else if (tabName === 'reports') {
|
||||||
|
loadReportsList();
|
||||||
|
} else if (tabName === 'batch') {
|
||||||
|
loadBatchModels();
|
||||||
|
loadBatchStatus();
|
||||||
|
startBatchRefresh();
|
||||||
|
} else {
|
||||||
|
stopBatchRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load home statistics
|
||||||
|
async function loadHomeStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/dashboard/statistics');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('homeModelsCount').textContent = data.models.total;
|
||||||
|
document.getElementById('homePredictionsCount').textContent = data.predictions.total;
|
||||||
|
document.getElementById('homeReportsCount').textContent = data.reports.total;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading home stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load models list
|
||||||
|
async function loadModelsList() {
|
||||||
|
const loading = document.getElementById('modelsLoading');
|
||||||
|
const list = document.getElementById('modelsList');
|
||||||
|
const tbody = document.getElementById('modelsTableBody');
|
||||||
|
|
||||||
|
loading.style.display = 'block';
|
||||||
|
list.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/models/list');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
data.models.forEach(model => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const accuracy = model.info.metrics?.accuracy
|
||||||
|
? (model.info.metrics.accuracy * 100).toFixed(2) + '%'
|
||||||
|
: 'N/A';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><strong>${model.filename}</strong></td>
|
||||||
|
<td>${model.info.model_type || 'N/A'}</td>
|
||||||
|
<td><span style="color: #4caf50; font-weight: 600;">${accuracy}</span></td>
|
||||||
|
<td>${new Date(model.created).toLocaleString('vi-VN')}</td>
|
||||||
|
<td>${model.size_mb} MB</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9em;"
|
||||||
|
onclick="window.open('/api/reports/view/training_report_${model.filename.replace('.joblib', '')}.html', '_blank')">
|
||||||
|
📄 Report
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
loading.style.display = 'none';
|
||||||
|
list.style.display = 'block';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading models:', error);
|
||||||
|
loading.innerHTML = '<p style="color: #f44336;">❌ Lỗi khi tải danh sách models</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reports list
|
||||||
|
async function loadReportsList() {
|
||||||
|
const loading = document.getElementById('reportsLoading');
|
||||||
|
const list = document.getElementById('reportsList');
|
||||||
|
const tbody = document.getElementById('reportsTableBody');
|
||||||
|
|
||||||
|
loading.style.display = 'block';
|
||||||
|
list.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/reports/list');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
data.reports.forEach(report => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const typeIcon = report.type === 'training' ? '🎓' : '🗺️';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><strong>${report.filename}</strong></td>
|
||||||
|
<td>${typeIcon} ${report.type}</td>
|
||||||
|
<td>${new Date(report.created).toLocaleString('vi-VN')}</td>
|
||||||
|
<td>${report.size_kb} KB</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9em;"
|
||||||
|
onclick="window.open('${report.view_url}', '_blank')">
|
||||||
|
👁️ Xem
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" style="padding: 8px 16px; font-size: 0.9em; margin-left: 5px;"
|
||||||
|
onclick="window.location.href='${report.download_url}'">
|
||||||
|
💾 Download
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
loading.style.display = 'none';
|
||||||
|
list.style.display = 'block';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading reports:', error);
|
||||||
|
loading.innerHTML = '<p style="color: #f44336;">❌ Lỗi khi tải danh sách reports</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch processing functions
|
||||||
|
async function loadBatchModels() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/models/list');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const select = document.getElementById('batchModelSelect');
|
||||||
|
select.innerHTML = '<option value="">Chọn model...</option>';
|
||||||
|
|
||||||
|
data.models.forEach(model => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = model.filename;
|
||||||
|
option.textContent = `${model.filename} (${model.created})`;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading batch models:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchCSV(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
const text = e.target.result;
|
||||||
|
const lines = text.trim().split('\n');
|
||||||
|
batchCSVData = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const parts = lines[i].split(',');
|
||||||
|
if (parts.length >= 5) {
|
||||||
|
batchCSVData.push({
|
||||||
|
name: parts[0].trim(),
|
||||||
|
min_lon: parseFloat(parts[1]),
|
||||||
|
min_lat: parseFloat(parts[2]),
|
||||||
|
max_lon: parseFloat(parts[3]),
|
||||||
|
max_lat: parseFloat(parts[4]),
|
||||||
|
start_date: parts[5]?.trim() || "2023-03-01",
|
||||||
|
end_date: parts[6]?.trim() || "2023-05-31",
|
||||||
|
max_scenes: parseInt(parts[7]) || 12,
|
||||||
|
cloud_cover: parseInt(parts[8]) || 30,
|
||||||
|
resolution: parseInt(parts[9]) || 20
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`✅ Đã tải ${batchCSVData.length} khu vực từ CSV`);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startBatch() {
|
||||||
|
const modelFilename = document.getElementById('batchModelSelect').value;
|
||||||
|
|
||||||
|
if (!modelFilename) {
|
||||||
|
alert('❌ Vui lòng chọn model');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batchCSVData.length === 0) {
|
||||||
|
alert('❌ Vui lòng upload file CSV trước');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/batch/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model_filename: modelFilename,
|
||||||
|
items: batchCSVData,
|
||||||
|
auto_retry: true,
|
||||||
|
max_retries: 3
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
alert(`✅ ${result.message}`);
|
||||||
|
loadBatchStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting batch:', error);
|
||||||
|
alert('❌ Lỗi khi bắt đầu batch prediction');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBatchStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/batch/status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('batchQueued').textContent = data.queue.queued;
|
||||||
|
document.getElementById('batchRunning').textContent = data.queue.running;
|
||||||
|
document.getElementById('batchCompleted').textContent = data.queue.completed;
|
||||||
|
document.getElementById('batchFailed').textContent = data.queue.failed;
|
||||||
|
|
||||||
|
// Display jobs
|
||||||
|
const jobsList = document.getElementById('batchJobsList');
|
||||||
|
const allJobs = [
|
||||||
|
...data.jobs.running,
|
||||||
|
...data.jobs.queued,
|
||||||
|
...data.jobs.recent_completed.slice(0, 5)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allJobs.length === 0) {
|
||||||
|
jobsList.innerHTML = '<p style="text-align: center; color: #666;">Chưa có job nào</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobsList.innerHTML = allJobs.map(job => `
|
||||||
|
<div class="card" style="margin-bottom: 15px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<strong>${job.name}</strong>
|
||||||
|
<span class="status-badge ${job.status}">${job.status.toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
<p style="color: #666; margin: 10px 0;">
|
||||||
|
📍 [${job.config.min_lon.toFixed(2)}, ${job.config.min_lat.toFixed(2)}] →
|
||||||
|
[${job.config.max_lon.toFixed(2)}, ${job.config.max_lat.toFixed(2)}]
|
||||||
|
</p>
|
||||||
|
${job.error ? `<p style="color: #f44336;">⚠️ ${job.error}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading batch status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startBatchRefresh() {
|
||||||
|
if (refreshInterval) return;
|
||||||
|
refreshInterval = setInterval(loadBatchStatus, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopBatchRefresh() {
|
||||||
|
if (refreshInterval) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
refreshInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
window.onload = function() {
|
||||||
|
loadHomeStats();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup on page unload
|
||||||
|
window.onbeforeunload = function() {
|
||||||
|
stopBatchRefresh();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,803 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Prediction Interface - Land Classification</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#predictMap {
|
||||||
|
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-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
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.predicting {
|
||||||
|
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%);
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card h4 {
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card .value {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
border-left: 4px solid #0c5460;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #d4edda;
|
||||||
|
border-left: 4px solid #155724;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-left: 4px solid #721c24;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.predictions-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-item {
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-item:hover {
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🗺️ Prediction Interface</h1>
|
||||||
|
<p>Phân loại đất cho khu vực mới sử dụng model đã train</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<!-- Map Section -->
|
||||||
|
<div class="map-container">
|
||||||
|
<div class="map-instructions">
|
||||||
|
<h3>📍 Chọn khu vực để predict</h3>
|
||||||
|
<p>✏️ Click vào nút hình vuông bên phải để vẽ bbox</p>
|
||||||
|
<p>🖱️ Kéo và thả để tạo vùng muốn phân loại</p>
|
||||||
|
<p>🔄 Có thể chỉnh sửa sau khi vẽ</p>
|
||||||
|
</div>
|
||||||
|
<div id="predictMap"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Model Selection -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>🤖 Chọn Model</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="modelSelect">Model đã train:</label>
|
||||||
|
<select id="modelSelect">
|
||||||
|
<option value="">Đang tải...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- Cache selection dropdown -->
|
||||||
|
<div class="form-group" style="margin-top:15px;">
|
||||||
|
<label for="cacheSelect">Chọn cache dữ liệu đầu vào:</label>
|
||||||
|
<select id="cacheSelect">
|
||||||
|
<option value="">-- Không dùng cache --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="modelInfo" style="display: none; background: #e8f5e9; padding: 15px; border-radius: 8px; margin-top: 15px;">
|
||||||
|
<h4 style="color: #2e7d32; margin-bottom: 10px;">📊 Thông tin Model</h4>
|
||||||
|
<p><strong>Type:</strong> <span id="modelType">-</span></p>
|
||||||
|
<p><strong>Accuracy:</strong> <span id="modelAccuracy">-</span></p>
|
||||||
|
<p><strong>Training Date:</strong> <span id="modelDate">-</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Time & Data Configuration -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>⏰ Thời gian & Dữ liệu</h2>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="predStartDate">Từ ngày:</label>
|
||||||
|
<input type="date" id="predStartDate" value="2023-03-01">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="predEndDate">Đến ngày:</label>
|
||||||
|
<input type="date" id="predEndDate" value="2023-05-31">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="predMaxScenes">Max Scenes:</label>
|
||||||
|
<input type="number" id="predMaxScenes" value="12" min="1" max="100">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="predCloudCover">Cloud Cover (%):</label>
|
||||||
|
<input type="number" id="predCloudCover" value="30" min="0" max="100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="predResolution">Resolution:</label>
|
||||||
|
<select id="predResolution">
|
||||||
|
<option value="10">10m (Chi tiết cao - Chậm)</option>
|
||||||
|
<option value="20" selected>20m (Cân bằng)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" onclick="startPrediction()" id="predictBtn">
|
||||||
|
🚀 Start Prediction
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Section -->
|
||||||
|
<div class="section" style="grid-column: 1 / -1;">
|
||||||
|
<h2>📊 Trạng thái Prediction</h2>
|
||||||
|
|
||||||
|
<div id="predictionStatus" class="status-box" style="display: none;">
|
||||||
|
<h3>⏳ Đang xử lý...</h3>
|
||||||
|
<p id="predictionProgress">Đang khởi tạo...</p>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" id="predictionProgressBar">0%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="predictionResult" style="display: none;">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<h3>✅ Prediction hoàn thành!</h3>
|
||||||
|
<p><strong>Output file:</strong> <span id="resultFile"></span></p>
|
||||||
|
<p><strong>Shape:</strong> <span id="resultShape"></span></p>
|
||||||
|
<p><strong>Unique classes:</strong> <span id="resultClasses"></span></p>
|
||||||
|
|
||||||
|
<!-- PNG Preview -->
|
||||||
|
<div id="pngPreviewContainer" style="display: none; margin: 20px 0;">
|
||||||
|
<h4 style="margin-bottom: 10px;">🖼️ Preview:</h4>
|
||||||
|
<img id="pngPreview" style="max-width: 100%; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.2);" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 15px;">
|
||||||
|
<button class="btn btn-success" onclick="downloadPrediction()">
|
||||||
|
💾 Download GeoTIFF
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="viewReport()">
|
||||||
|
📄 View Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="predictionError" class="alert alert-danger" style="display: none;">
|
||||||
|
<h3>❌ Lỗi</h3>
|
||||||
|
<p id="errorMessage"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Previous Predictions -->
|
||||||
|
<div class="section" style="grid-column: 1 / -1;">
|
||||||
|
<h2>📋 Predictions đã tạo</h2>
|
||||||
|
<div id="predictionsList" class="predictions-list">
|
||||||
|
<p style="text-align: center; color: #666;">Đang tải...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<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>
|
||||||
|
// Map setup
|
||||||
|
let map, drawnItems, drawControl;
|
||||||
|
let selectedBbox = null;
|
||||||
|
let currentPredictionFile = null;
|
||||||
|
let currentReportFile = null;
|
||||||
|
let statusCheckInterval = null;
|
||||||
|
|
||||||
|
// Initialize map
|
||||||
|
function initMap() {
|
||||||
|
map = L.map('predictMap').setView([9.5, 105.9], 9);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Initialize drawing
|
||||||
|
drawnItems = new L.FeatureGroup();
|
||||||
|
map.addLayer(drawnItems);
|
||||||
|
|
||||||
|
drawControl = new L.Control.Draw({
|
||||||
|
draw: {
|
||||||
|
rectangle: true,
|
||||||
|
polygon: false,
|
||||||
|
circle: false,
|
||||||
|
marker: false,
|
||||||
|
polyline: false,
|
||||||
|
circlemarker: false
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
featureGroup: drawnItems,
|
||||||
|
remove: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addControl(drawControl);
|
||||||
|
|
||||||
|
// Handle drawing
|
||||||
|
map.on(L.Draw.Event.CREATED, function(event) {
|
||||||
|
drawnItems.clearLayers();
|
||||||
|
const layer = event.layer;
|
||||||
|
drawnItems.addLayer(layer);
|
||||||
|
|
||||||
|
const bounds = layer.getBounds();
|
||||||
|
let bbox = {
|
||||||
|
min_lon: bounds.getWest(),
|
||||||
|
min_lat: bounds.getSouth(),
|
||||||
|
max_lon: bounds.getEast(),
|
||||||
|
max_lat: bounds.getNorth()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate bbox (must be within valid geographic coordinates)
|
||||||
|
if (bbox.min_lon < -180 || bbox.max_lon > 180 || bbox.min_lat < -90 || bbox.max_lat > 90) {
|
||||||
|
alert('❌ Bbox không hợp lệ! Vui lòng vẽ trong phạm vi bản đồ hợp lệ.\nKinh độ: -180 đến 180, Vĩ độ: -90 đến 90');
|
||||||
|
drawnItems.clearLayers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedBbox = bbox;
|
||||||
|
// Cache bbox to localStorage
|
||||||
|
localStorage.setItem('prediction_bbox', JSON.stringify(selectedBbox));
|
||||||
|
console.log('Selected bbox:', selectedBbox);
|
||||||
|
});
|
||||||
|
|
||||||
|
// On load, restore bbox from cache if exists
|
||||||
|
const cachedBbox = localStorage.getItem('prediction_bbox');
|
||||||
|
if (cachedBbox) {
|
||||||
|
try {
|
||||||
|
const bbox = JSON.parse(cachedBbox);
|
||||||
|
|
||||||
|
// Validate bbox before restoring
|
||||||
|
if (bbox.min_lon < -180 || bbox.max_lon > 180 ||
|
||||||
|
bbox.min_lat < -90 || bbox.max_lat > 90) {
|
||||||
|
console.warn('Cache bbox không hợp lệ, đã xóa:', bbox);
|
||||||
|
localStorage.removeItem('prediction_bbox');
|
||||||
|
} else {
|
||||||
|
// Draw rectangle on map
|
||||||
|
const bounds = [
|
||||||
|
[bbox.min_lat, bbox.min_lon],
|
||||||
|
[bbox.max_lat, bbox.max_lon]
|
||||||
|
];
|
||||||
|
const rectangle = L.rectangle(bounds, {
|
||||||
|
color: '#667eea',
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
});
|
||||||
|
drawnItems.addLayer(rectangle);
|
||||||
|
map.fitBounds(bounds);
|
||||||
|
selectedBbox = bbox;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Không thể khôi phục bbox từ cache:', e);
|
||||||
|
localStorage.removeItem('prediction_bbox');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load models list
|
||||||
|
async function loadModels() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/models/list');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const select = document.getElementById('modelSelect');
|
||||||
|
select.innerHTML = '<option value="">Chọn model...</option>';
|
||||||
|
|
||||||
|
// Chỉ lấy các file model thực sự (.joblib), loại bỏ các file có chứa '_info.joblib'
|
||||||
|
data.models
|
||||||
|
.filter(m => m.filename.endsWith('.joblib') && !m.filename.includes('_info.joblib'))
|
||||||
|
.forEach(model => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = model.filename;
|
||||||
|
option.textContent = `${model.filename} - ${model.created}`;
|
||||||
|
option.dataset.info = JSON.stringify(model.info);
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-select first model đúng
|
||||||
|
const firstJoblib = data.models.find(m => m.filename.endsWith('.joblib') && !m.filename.includes('_info.joblib'));
|
||||||
|
if (firstJoblib) {
|
||||||
|
select.value = firstJoblib.filename;
|
||||||
|
updateModelInfo();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading models:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update model info display
|
||||||
|
function updateModelInfo() {
|
||||||
|
const select = document.getElementById('modelSelect');
|
||||||
|
const option = select.options[select.selectedIndex];
|
||||||
|
|
||||||
|
if (option.dataset.info) {
|
||||||
|
const info = JSON.parse(option.dataset.info);
|
||||||
|
const infoDiv = document.getElementById('modelInfo');
|
||||||
|
|
||||||
|
document.getElementById('modelType').textContent = info.model_type || 'N/A';
|
||||||
|
document.getElementById('modelAccuracy').textContent = info.metrics?.accuracy
|
||||||
|
? (info.metrics.accuracy * 100).toFixed(2) + '%'
|
||||||
|
: 'N/A';
|
||||||
|
document.getElementById('modelDate').textContent = info.training_date || 'N/A';
|
||||||
|
|
||||||
|
infoDiv.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('modelInfo').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start prediction
|
||||||
|
async function startPrediction() {
|
||||||
|
if (!selectedBbox) {
|
||||||
|
alert('❌ Vui lòng vẽ bbox trên bản đồ trước!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate bbox before sending
|
||||||
|
if (selectedBbox.min_lon < -180 || selectedBbox.max_lon > 180 ||
|
||||||
|
selectedBbox.min_lat < -90 || selectedBbox.max_lat > 90) {
|
||||||
|
alert('❌ Bbox không hợp lệ! Vui lòng vẽ lại trong phạm vi bản đồ hợp lệ.');
|
||||||
|
drawnItems.clearLayers();
|
||||||
|
selectedBbox = null;
|
||||||
|
localStorage.removeItem('prediction_bbox');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelFilename = document.getElementById('modelSelect').value;
|
||||||
|
if (!modelFilename) {
|
||||||
|
alert('❌ Vui lòng chọn model!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
model_filename: modelFilename,
|
||||||
|
min_lon: selectedBbox.min_lon,
|
||||||
|
min_lat: selectedBbox.min_lat,
|
||||||
|
max_lon: selectedBbox.max_lon,
|
||||||
|
max_lat: selectedBbox.max_lat,
|
||||||
|
start_date: document.getElementById('predStartDate').value,
|
||||||
|
end_date: document.getElementById('predEndDate').value,
|
||||||
|
max_scenes: parseInt(document.getElementById('predMaxScenes').value),
|
||||||
|
cloud_cover: parseInt(document.getElementById('predCloudCover').value),
|
||||||
|
resolution: parseInt(document.getElementById('predResolution').value)
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.getElementById('predictBtn').disabled = true;
|
||||||
|
document.getElementById('predictionStatus').style.display = 'block';
|
||||||
|
document.getElementById('predictionResult').style.display = 'none';
|
||||||
|
document.getElementById('predictionError').style.display = 'none';
|
||||||
|
|
||||||
|
const response = await fetch('/api/prediction/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Start monitoring status
|
||||||
|
startStatusCheck();
|
||||||
|
} else {
|
||||||
|
throw new Error(result.detail || 'Lỗi khi bắt đầu prediction');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting prediction:', error);
|
||||||
|
document.getElementById('predictionError').style.display = 'block';
|
||||||
|
document.getElementById('errorMessage').textContent = error.message;
|
||||||
|
document.getElementById('predictBtn').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check prediction status
|
||||||
|
async function checkStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/prediction/status');
|
||||||
|
const status = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('predictionProgress').textContent = status.progress;
|
||||||
|
|
||||||
|
// Update progress bar (estimate based on message)
|
||||||
|
let progress = 0;
|
||||||
|
if (status.progress.includes('khởi')) progress = 10;
|
||||||
|
else if (status.progress.includes('Sentinel-2')) progress = 30;
|
||||||
|
else if (status.progress.includes('NDVI')) progress = 50;
|
||||||
|
else if (status.progress.includes('Sentinel-1')) progress = 60;
|
||||||
|
else if (status.progress.includes('features')) progress = 70;
|
||||||
|
else if (status.progress.includes('dự đoán')) progress = 80;
|
||||||
|
else if (status.progress.includes('lưu')) progress = 90;
|
||||||
|
else if (status.progress.includes('Hoàn thành')) progress = 100;
|
||||||
|
|
||||||
|
document.getElementById('predictionProgressBar').style.width = progress + '%';
|
||||||
|
document.getElementById('predictionProgressBar').textContent = progress + '%';
|
||||||
|
|
||||||
|
if (!status.is_predicting) {
|
||||||
|
stopStatusCheck();
|
||||||
|
document.getElementById('predictBtn').disabled = false;
|
||||||
|
|
||||||
|
if (status.error) {
|
||||||
|
document.getElementById('predictionStatus').style.display = 'none';
|
||||||
|
document.getElementById('predictionError').style.display = 'block';
|
||||||
|
document.getElementById('errorMessage').textContent = status.error;
|
||||||
|
} else if (status.result) {
|
||||||
|
document.getElementById('predictionStatus').style.display = 'none';
|
||||||
|
document.getElementById('predictionResult').style.display = 'block';
|
||||||
|
|
||||||
|
currentPredictionFile = status.result.output_file;
|
||||||
|
currentReportFile = status.result.report_filename;
|
||||||
|
|
||||||
|
document.getElementById('resultFile').textContent = status.result.output_file;
|
||||||
|
document.getElementById('resultShape').textContent = status.result.shape.join(' x ');
|
||||||
|
document.getElementById('resultClasses').textContent = status.result.unique_classes.join(', ');
|
||||||
|
|
||||||
|
// Show PNG preview if available
|
||||||
|
if (status.result.png_file) {
|
||||||
|
const pngFilename = status.result.png_file.split('/').pop();
|
||||||
|
const previewImg = document.getElementById('pngPreview');
|
||||||
|
const previewContainer = document.getElementById('pngPreviewContainer');
|
||||||
|
|
||||||
|
previewImg.src = `/api/predictions/preview/${pngFilename}`;
|
||||||
|
previewContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload predictions list
|
||||||
|
loadPredictionsList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start/stop status monitoring
|
||||||
|
function startStatusCheck() {
|
||||||
|
if (statusCheckInterval) clearInterval(statusCheckInterval);
|
||||||
|
statusCheckInterval = setInterval(checkStatus, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopStatusCheck() {
|
||||||
|
if (statusCheckInterval) {
|
||||||
|
clearInterval(statusCheckInterval);
|
||||||
|
statusCheckInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download prediction
|
||||||
|
function downloadPrediction() {
|
||||||
|
if (currentPredictionFile) {
|
||||||
|
const filename = currentPredictionFile.split('/').pop();
|
||||||
|
window.location.href = `/api/predictions/download/${filename}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// View report
|
||||||
|
function viewReport() {
|
||||||
|
if (currentReportFile) {
|
||||||
|
window.open(`/api/reports/view/${currentReportFile}`, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load predictions list
|
||||||
|
async function loadPredictionsList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/predictions/list');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const listDiv = document.getElementById('predictionsList');
|
||||||
|
|
||||||
|
if (data.predictions.length === 0) {
|
||||||
|
listDiv.innerHTML = '<p style="text-align: center; color: #666;">Chưa có prediction nào</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listDiv.innerHTML = data.predictions.map(pred => `
|
||||||
|
<div class="prediction-item">
|
||||||
|
<div>
|
||||||
|
<strong>${pred.filename}</strong>
|
||||||
|
<br>
|
||||||
|
<small style="color: #666;">
|
||||||
|
${new Date(pred.created).toLocaleString('vi-VN')} - ${pred.size_mb} MB
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-success" style="padding: 8px 16px; font-size: 0.9em;"
|
||||||
|
onclick="window.location.href='${pred.download_url}'">
|
||||||
|
💾 Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading predictions:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load cache list
|
||||||
|
async function loadCacheList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/cache/info');
|
||||||
|
const data = await response.json();
|
||||||
|
const select = document.getElementById('cacheSelect');
|
||||||
|
select.innerHTML = '<option value="">-- Không dùng cache --</option>';
|
||||||
|
if (data.files && data.files.length > 0) {
|
||||||
|
data.files.forEach((file, idx) => {
|
||||||
|
if (file.filename.startsWith('prediction_input_')) {
|
||||||
|
let label = `#${idx+1} | ${file.filename}`;
|
||||||
|
if (file.metadata && file.metadata.bbox) {
|
||||||
|
label += ` | BBox: [${file.metadata.bbox.join(', ')}]`;
|
||||||
|
}
|
||||||
|
if (file.metadata && file.metadata.time_range) {
|
||||||
|
label += ` | Time: ${file.metadata.time_range}`;
|
||||||
|
}
|
||||||
|
select.innerHTML += `<option value="${file.filename}">${label}</option>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Không thể tải danh sách cache:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
window.onload = function() {
|
||||||
|
initMap();
|
||||||
|
loadModels();
|
||||||
|
loadPredictionsList();
|
||||||
|
loadCacheList();
|
||||||
|
// Add event listener for model selection
|
||||||
|
document.getElementById('modelSelect').addEventListener('change', updateModelInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup on page unload
|
||||||
|
window.onbeforeunload = function() {
|
||||||
|
stopStatusCheck();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Prediction Report - 20251221_171732</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #ff6b6b;
|
||||||
|
border-bottom: 3px solid #ff6b6b;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ff6b6b30;
|
||||||
|
}
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
.stat-card .label {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background: #fff3cd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-left: 5px solid #ff6b6b;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 200px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.class-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🗺️ Báo Cáo Dự Đoán</h1>
|
||||||
|
<p>Land Classification Prediction - 21/12/2025 17:17:32</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="section">
|
||||||
|
<h2>📈 Tóm Tắt Kết Quả</h2>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">420</div>
|
||||||
|
<div class="label">Tổng số Pixels</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">20x21</div>
|
||||||
|
<div class="label">Kích thước (px)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">0.1</div>
|
||||||
|
<div class="label">Diện tích (km²)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">1</div>
|
||||||
|
<div class="label">Số Classes</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">3</div>
|
||||||
|
<div class="label">Số Features</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">✅</div>
|
||||||
|
<div class="label">Sử dụng Radar</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>⚙️ Thông Tin Chi Tiết</h2>
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">🤖 Model sử dụng:</span>
|
||||||
|
<span>model_cnn_20251221_163841.joblib</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📍 Khu vực (bbox):</span>
|
||||||
|
<span>[105.16372919082643, 9.182049314243548, 105.16746282577516, 9.185480898286633]</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📅 Thời gian:</span>
|
||||||
|
<span>2023-03-01/2023-05-31</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">💾 Output file:</span>
|
||||||
|
<span>predictions/prediction_20251221_171732.tif</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🏷️ Các Classes Phát Hiện</h2>
|
||||||
|
<div>
|
||||||
|
<span class="class-badge">6</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>🌍 Land Classification System | Generated: 21/12/2025 17:17:32</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Prediction Report - 20251221_172119</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #ff6b6b;
|
||||||
|
border-bottom: 3px solid #ff6b6b;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ff6b6b30;
|
||||||
|
}
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
.stat-card .label {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background: #fff3cd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-left: 5px solid #ff6b6b;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 200px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.class-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🗺️ Báo Cáo Dự Đoán</h1>
|
||||||
|
<p>Land Classification Prediction - 21/12/2025 17:21:19</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="section">
|
||||||
|
<h2>📈 Tóm Tắt Kết Quả</h2>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">420</div>
|
||||||
|
<div class="label">Tổng số Pixels</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">20x21</div>
|
||||||
|
<div class="label">Kích thước (px)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">0.1</div>
|
||||||
|
<div class="label">Diện tích (km²)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">1</div>
|
||||||
|
<div class="label">Số Classes</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">3</div>
|
||||||
|
<div class="label">Số Features</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">✅</div>
|
||||||
|
<div class="label">Sử dụng Radar</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>⚙️ Thông Tin Chi Tiết</h2>
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">🤖 Model sử dụng:</span>
|
||||||
|
<span>model_cnn_20251221_163841.joblib</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📍 Khu vực (bbox):</span>
|
||||||
|
<span>[105.16372919082643, 9.182049314243548, 105.16746282577516, 9.185480898286633]</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📅 Thời gian:</span>
|
||||||
|
<span>2023-03-01/2023-05-31</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">💾 Output file:</span>
|
||||||
|
<span>predictions/prediction_20251221_172118.tif</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🏷️ Các Classes Phát Hiện</h2>
|
||||||
|
<div>
|
||||||
|
<span class="class-badge">6</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>🌍 Land Classification System | Generated: 21/12/2025 17:21:19</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Prediction Report - 20251221_172815</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #ff6b6b;
|
||||||
|
border-bottom: 3px solid #ff6b6b;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ff6b6b30;
|
||||||
|
}
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
.stat-card .label {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background: #fff3cd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-left: 5px solid #ff6b6b;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 200px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.class-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🗺️ Báo Cáo Dự Đoán</h1>
|
||||||
|
<p>Land Classification Prediction - 21/12/2025 17:28:15</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="section">
|
||||||
|
<h2>📈 Tóm Tắt Kết Quả</h2>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">420</div>
|
||||||
|
<div class="label">Tổng số Pixels</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">20x21</div>
|
||||||
|
<div class="label">Kích thước (px)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">0.1</div>
|
||||||
|
<div class="label">Diện tích (km²)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">2</div>
|
||||||
|
<div class="label">Số Classes</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">3</div>
|
||||||
|
<div class="label">Số Features</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">✅</div>
|
||||||
|
<div class="label">Sử dụng Radar</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>⚙️ Thông Tin Chi Tiết</h2>
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">🤖 Model sử dụng:</span>
|
||||||
|
<span>model_xgboost_20251221_172351.joblib</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📍 Khu vực (bbox):</span>
|
||||||
|
<span>[105.16372919082643, 9.182049314243548, 105.16746282577516, 9.185480898286633]</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📅 Thời gian:</span>
|
||||||
|
<span>2023-03-01/2023-05-31</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">💾 Output file:</span>
|
||||||
|
<span>predictions/prediction_20251221_172814.tif</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🏷️ Các Classes Phát Hiện</h2>
|
||||||
|
<div>
|
||||||
|
<span class="class-badge">3</span><span class="class-badge">6</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>🌍 Land Classification System | Generated: 21/12/2025 17:28:15</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Prediction Report - 20251221_172829</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #ff6b6b;
|
||||||
|
border-bottom: 3px solid #ff6b6b;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ff6b6b30;
|
||||||
|
}
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
.stat-card .label {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background: #fff3cd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-left: 5px solid #ff6b6b;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 200px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.class-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🗺️ Báo Cáo Dự Đoán</h1>
|
||||||
|
<p>Land Classification Prediction - 21/12/2025 17:28:29</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="section">
|
||||||
|
<h2>📈 Tóm Tắt Kết Quả</h2>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">420</div>
|
||||||
|
<div class="label">Tổng số Pixels</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">20x21</div>
|
||||||
|
<div class="label">Kích thước (px)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">0.1</div>
|
||||||
|
<div class="label">Diện tích (km²)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">2</div>
|
||||||
|
<div class="label">Số Classes</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">3</div>
|
||||||
|
<div class="label">Số Features</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value">✅</div>
|
||||||
|
<div class="label">Sử dụng Radar</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>⚙️ Thông Tin Chi Tiết</h2>
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">🤖 Model sử dụng:</span>
|
||||||
|
<span>model_xgboost_20251221_172351.joblib</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📍 Khu vực (bbox):</span>
|
||||||
|
<span>[105.16372919082643, 9.182049314243548, 105.16746282577516, 9.185480898286633]</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📅 Thời gian:</span>
|
||||||
|
<span>2023-03-01/2023-05-31</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">💾 Output file:</span>
|
||||||
|
<span>predictions/prediction_20251221_172828.tif</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🏷️ Các Classes Phát Hiện</h2>
|
||||||
|
<div>
|
||||||
|
<span class="class-badge">3</span><span class="class-badge">6</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>🌍 Land Classification System | Generated: 21/12/2025 17:28:29</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Demo script để test các chức năng mới của API
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:8000"
|
||||||
|
|
||||||
|
def print_section(title):
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(f" {title}")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
def test_dashboard_statistics():
|
||||||
|
print_section("📊 Test Dashboard Statistics")
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{BASE_URL}/api/dashboard/statistics")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"✅ Success!")
|
||||||
|
print(f" Models: {data['models']['total']}")
|
||||||
|
print(f" Predictions: {data['predictions']['total']}")
|
||||||
|
print(f" Reports: {data['reports']['total']}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Exception: {e}")
|
||||||
|
|
||||||
|
def test_accuracy_trends():
|
||||||
|
print_section("📈 Test Accuracy Trends")
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{BASE_URL}/api/dashboard/accuracy-trends")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"✅ Success!")
|
||||||
|
print(f" Trends: {len(data['trends'])} records")
|
||||||
|
print(f" Models: {data['models']}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Exception: {e}")
|
||||||
|
|
||||||
|
def test_class_distribution():
|
||||||
|
print_section("📊 Test Class Distribution")
|
||||||
|
try:
|
||||||
|
# First, get list of models
|
||||||
|
response = requests.get(f"{BASE_URL}/api/models/list")
|
||||||
|
if response.status_code == 200:
|
||||||
|
models = response.json()['models']
|
||||||
|
if models:
|
||||||
|
model_filename = models[0]['filename']
|
||||||
|
print(f" Using model: {model_filename}")
|
||||||
|
|
||||||
|
# Get class distribution
|
||||||
|
response = requests.get(f"{BASE_URL}/api/dashboard/class-distribution/{model_filename}")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"✅ Success!")
|
||||||
|
print(f" Total samples: {data['total_samples']}")
|
||||||
|
print(f" Classes: {list(data['class_distribution'].keys())}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {response.status_code}")
|
||||||
|
else:
|
||||||
|
print("⚠️ No models found")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error getting models: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Exception: {e}")
|
||||||
|
|
||||||
|
def test_batch_status():
|
||||||
|
print_section("🔄 Test Batch Status")
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{BASE_URL}/api/batch/status")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"✅ Success!")
|
||||||
|
print(f" Queued: {data['queue']['queued']}")
|
||||||
|
print(f" Running: {data['queue']['running']}")
|
||||||
|
print(f" Completed: {data['queue']['completed']}")
|
||||||
|
print(f" Failed: {data['queue']['failed']}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Exception: {e}")
|
||||||
|
|
||||||
|
def test_batch_prediction_demo():
|
||||||
|
print_section("🚀 Test Batch Prediction (Demo)")
|
||||||
|
try:
|
||||||
|
# Get a model
|
||||||
|
response = requests.get(f"{BASE_URL}/api/models/list")
|
||||||
|
if response.status_code != 200:
|
||||||
|
print("❌ Cannot get models list")
|
||||||
|
return
|
||||||
|
|
||||||
|
models = response.json()['models']
|
||||||
|
if not models:
|
||||||
|
print("⚠️ No models available for testing")
|
||||||
|
return
|
||||||
|
|
||||||
|
model_filename = models[0]['filename']
|
||||||
|
print(f" Using model: {model_filename}")
|
||||||
|
|
||||||
|
# Create test batch
|
||||||
|
batch_config = {
|
||||||
|
"model_filename": model_filename,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Test_Region_1",
|
||||||
|
"min_lon": 105.6,
|
||||||
|
"min_lat": 9.3,
|
||||||
|
"max_lon": 105.7,
|
||||||
|
"max_lat": 9.4,
|
||||||
|
"start_date": "2023-03-01",
|
||||||
|
"end_date": "2023-03-31",
|
||||||
|
"max_scenes": 5,
|
||||||
|
"cloud_cover": 30,
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auto_retry": True,
|
||||||
|
"max_retries": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
print(" Creating batch job...")
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/api/batch/start",
|
||||||
|
json=batch_config
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"✅ Success!")
|
||||||
|
print(f" {data['message']}")
|
||||||
|
print(f" Batch ID: {data['batch_id']}")
|
||||||
|
|
||||||
|
# Check status after a moment
|
||||||
|
time.sleep(2)
|
||||||
|
response = requests.get(f"{BASE_URL}/api/batch/status")
|
||||||
|
if response.status_code == 200:
|
||||||
|
status = response.json()
|
||||||
|
print(f" Current queue: {status['queue']}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {response.status_code} - {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Exception: {e}")
|
||||||
|
|
||||||
|
def test_reports_list():
|
||||||
|
print_section("📄 Test Reports List")
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{BASE_URL}/api/reports/list")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"✅ Success!")
|
||||||
|
print(f" Total reports: {data['count']}")
|
||||||
|
if data['reports']:
|
||||||
|
print(f" Latest report: {data['reports'][0]['filename']}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Exception: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print(" 🧪 API Testing Suite - New Features")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"\n Base URL: {BASE_URL}")
|
||||||
|
print(f" Đảm bảo server đang chạy: python api_server.py")
|
||||||
|
|
||||||
|
input("\n Press ENTER to start testing...")
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
test_dashboard_statistics()
|
||||||
|
test_accuracy_trends()
|
||||||
|
test_class_distribution()
|
||||||
|
test_reports_list()
|
||||||
|
test_batch_status()
|
||||||
|
test_batch_prediction_demo()
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" ✅ Testing completed!")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"\n Dashboard: {BASE_URL}/dashboard")
|
||||||
|
print(f" API Docs: {BASE_URL}/docs")
|
||||||
|
print("=" * 70 + "\n")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+8
-438
@@ -286,7 +286,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>🌍 Land Classification Training</h1>
|
<h1>Training Interface</h1>
|
||||||
<p>Giao diện training model phân loại đất từ ảnh vệ tinh</p>
|
<p>Giao diện training model phân loại đất từ ảnh vệ tinh</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -507,6 +507,10 @@
|
|||||||
<div id="modelsList" class="model-list" style="margin-top: 15px;">
|
<div id="modelsList" class="model-list" style="margin-top: 15px;">
|
||||||
<p>Đang tải...</p>
|
<p>Đang tải...</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Reports Section -->
|
<!-- Reports Section -->
|
||||||
@@ -518,114 +522,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Prediction Section -->
|
|
||||||
<div class="section" style="grid-column: 1 / -1;">
|
|
||||||
<h2 style="text-align: center; margin-bottom: 30px;">🔮 Dự Đoán & Phân Loại (Prediction & Classification)</h2>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1.2fr 1fr; gap: 30px;">
|
|
||||||
<!-- Left: Prediction Map -->
|
|
||||||
<div>
|
|
||||||
<div style="background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%); padding: 20px; border-radius: 12px; border: 2px solid #ff6b6b40;">
|
|
||||||
<h3 style="margin: 0 0 15px 0; color: #ff6b6b; font-size: 18px;">🗺️ Bản Đồ Khu Vực Dự Đoán</h3>
|
|
||||||
|
|
||||||
<div class="map-instructions" style="background: #fff3cd; border-left: 4px solid #ff6b6b; margin-bottom: 15px;">
|
|
||||||
<strong>💡 Hướng dẫn:</strong> 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 #ff6b6b; vertical-align: middle; margin: 0 5px;">□</span>
|
|
||||||
để chọn khu vực cần dự đoán
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="predictionMap" style="height: 600px; border-radius: 8px; border: 3px solid #ff6b6b; box-shadow: 0 4px 12px rgba(255,107,107,0.3);"></div>
|
|
||||||
|
|
||||||
<div style="margin-top: 15px; padding: 12px; background: white; border-radius: 6px; border: 1px solid #ddd;">
|
|
||||||
<strong style="color: #ff6b6b;">📍 Tọa độ khu vực:</strong><br>
|
|
||||||
<span id="predBboxDisplay" style="font-family: monospace; color: #333; font-size: 13px;">Chưa chọn khu vực</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right: Configuration & Controls -->
|
|
||||||
<div>
|
|
||||||
<!-- Prediction Status -->
|
|
||||||
<div id="predictionStatusBox" class="status-box" style="margin-bottom: 20px;">
|
|
||||||
<p><strong>Trạng thái:</strong> <span id="predictionStatusText">Chưa bắt đầu</span></p>
|
|
||||||
<p><strong>Tiến độ:</strong> <span id="predictionProgressText">-</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Prediction Configuration -->
|
|
||||||
<form id="predictionForm">
|
|
||||||
<h3 style="margin-bottom: 15px; color: #ff6b6b;">🤖 Chọn Model</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Model để sử dụng:</label>
|
|
||||||
<select id="selectedModel" required style="border-color: #ff6b6b;">
|
|
||||||
<option value="">-- Chọn model --</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Hidden inputs for prediction bbox -->
|
|
||||||
<input type="hidden" id="predMinLon" value="105.6" required>
|
|
||||||
<input type="hidden" id="predMinLat" value="9.3" required>
|
|
||||||
<input type="hidden" id="predMaxLon" value="106.2" required>
|
|
||||||
<input type="hidden" id="predMaxLat" value="9.8" required>
|
|
||||||
|
|
||||||
<h3 style="margin: 20px 0 15px; color: #ff6b6b;">📅 Thời Gian Dự Đoán</h3>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Ngày bắt đầu:</label>
|
|
||||||
<input type="date" id="predStartDate" value="2023-03-01" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Ngày kết thúc:</label>
|
|
||||||
<input type="date" id="predEndDate" value="2023-05-31" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 style="margin: 20px 0 15px; color: #ff6b6b;">🛰️ 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="predMaxScenes" value="12" min="1" max="100" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Cloud cover (%):</label>
|
|
||||||
<input type="number" id="predCloudCover" value="30" min="0" max="100" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Độ phân giải (m):</label>
|
|
||||||
<select id="predResolution" 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 style="margin-top: 30px; text-align: center;">
|
|
||||||
<button type="submit" class="btn btn-primary" id="predictBtn" style="background: linear-gradient(135deg, #ff6b6b, #ee5a6f); width: 100%; padding: 15px; font-size: 16px; font-weight: 600;">
|
|
||||||
🔮 Bắt Đầu Dự Đoán
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Prediction Result -->
|
|
||||||
<div id="predictionResult" style="margin-top: 20px; display: none;">
|
|
||||||
<h3 style="color: #28a745; margin-bottom: 10px;">✅ Kết Quả Dự Đoán</h3>
|
|
||||||
<div style="background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); padding: 20px; border-radius: 8px; border: 2px solid #28a745;">
|
|
||||||
<div id="predResultText" style="font-size: 14px; line-height: 1.8;"></div>
|
|
||||||
<div id="downloadLinkContainer" style="margin-top: 15px; text-align: center;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Previous Predictions List -->
|
|
||||||
<div id="predictionsListSection" style="margin-top: 20px;">
|
|
||||||
<h3 style="color: #ff6b6b; margin-bottom: 10px;">📂 Các File Dự Đoán Đã Tạo</h3>
|
|
||||||
<div id="predictionsList" style="background: #f8f9fa; padding: 15px; border-radius: 8px; max-height: 200px; overflow-y: auto;">
|
|
||||||
<p style="color: #666; text-align: center;">Đang tải...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -644,15 +541,15 @@
|
|||||||
times: []
|
times: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load presets on page load
|
// Load presets and models only after DOM is ready
|
||||||
window.onload = async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await loadPresets();
|
await loadPresets();
|
||||||
await loadModels();
|
await loadModels();
|
||||||
await loadReports();
|
await loadReports();
|
||||||
await loadSystemInfo();
|
await loadSystemInfo();
|
||||||
checkStatus();
|
checkStatus();
|
||||||
loadTrainingHistory();
|
loadTrainingHistory();
|
||||||
};
|
});
|
||||||
|
|
||||||
// Load preset configurations
|
// Load preset configurations
|
||||||
async function loadPresets() {
|
async function loadPresets() {
|
||||||
@@ -1387,333 +1284,6 @@
|
|||||||
document.getElementById('cachePreset').addEventListener('change', applyCachePreset);
|
document.getElementById('cachePreset').addEventListener('change', applyCachePreset);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============== PREDICTION FUNCTIONALITY ==============
|
|
||||||
|
|
||||||
let predictionMap, predictionDrawnItems, predictionRectangle;
|
|
||||||
let predictionStatusInterval = null;
|
|
||||||
|
|
||||||
// Initialize prediction map
|
|
||||||
function initPredictionMap() {
|
|
||||||
predictionMap = L.map('predictionMap').setView([9.55, 105.9], 9);
|
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
attribution: '© OpenStreetMap contributors',
|
|
||||||
maxZoom: 18
|
|
||||||
}).addTo(predictionMap);
|
|
||||||
|
|
||||||
predictionDrawnItems = new L.FeatureGroup();
|
|
||||||
predictionMap.addLayer(predictionDrawnItems);
|
|
||||||
|
|
||||||
const drawControl = new L.Control.Draw({
|
|
||||||
draw: {
|
|
||||||
polyline: false,
|
|
||||||
polygon: false,
|
|
||||||
circle: false,
|
|
||||||
marker: false,
|
|
||||||
circlemarker: false,
|
|
||||||
rectangle: {
|
|
||||||
shapeOptions: {
|
|
||||||
color: '#ff6b6b',
|
|
||||||
weight: 3,
|
|
||||||
fillOpacity: 0.2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
edit: {
|
|
||||||
featureGroup: predictionDrawnItems,
|
|
||||||
remove: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
predictionMap.addControl(drawControl);
|
|
||||||
|
|
||||||
predictionMap.on(L.Draw.Event.CREATED, function(event) {
|
|
||||||
const layer = event.layer;
|
|
||||||
if (predictionRectangle) {
|
|
||||||
predictionDrawnItems.removeLayer(predictionRectangle);
|
|
||||||
}
|
|
||||||
predictionDrawnItems.addLayer(layer);
|
|
||||||
predictionRectangle = layer;
|
|
||||||
const bounds = layer.getBounds();
|
|
||||||
updatePredictionBbox(bounds);
|
|
||||||
});
|
|
||||||
|
|
||||||
predictionMap.on(L.Draw.Event.EDITED, function(event) {
|
|
||||||
const layers = event.layers;
|
|
||||||
layers.eachLayer(function(layer) {
|
|
||||||
const bounds = layer.getBounds();
|
|
||||||
updatePredictionBbox(bounds);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
predictionMap.on(L.Draw.Event.DELETED, function() {
|
|
||||||
predictionRectangle = null;
|
|
||||||
document.getElementById('predBboxDisplay').textContent = 'Chưa chọn khu vực';
|
|
||||||
document.getElementById('predMinLon').value = '';
|
|
||||||
document.getElementById('predMinLat').value = '';
|
|
||||||
document.getElementById('predMaxLon').value = '';
|
|
||||||
document.getElementById('predMaxLat').value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
drawInitialPredictionRectangle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePredictionBbox(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('predMinLat').value = south;
|
|
||||||
document.getElementById('predMinLon').value = west;
|
|
||||||
document.getElementById('predMaxLat').value = north;
|
|
||||||
document.getElementById('predMaxLon').value = east;
|
|
||||||
|
|
||||||
document.getElementById('predBboxDisplay').textContent =
|
|
||||||
`Lon: ${west} → ${east}, Lat: ${south} → ${north}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawInitialPredictionRectangle() {
|
|
||||||
const minLon = parseFloat(document.getElementById('predMinLon').value);
|
|
||||||
const minLat = parseFloat(document.getElementById('predMinLat').value);
|
|
||||||
const maxLon = parseFloat(document.getElementById('predMaxLon').value);
|
|
||||||
const maxLat = parseFloat(document.getElementById('predMaxLat').value);
|
|
||||||
|
|
||||||
if (minLon && minLat && maxLon && maxLat) {
|
|
||||||
const bounds = [[minLat, minLon], [maxLat, maxLon]];
|
|
||||||
const rectangle = L.rectangle(bounds, {
|
|
||||||
color: '#ff6b6b',
|
|
||||||
weight: 3,
|
|
||||||
fillOpacity: 0.2
|
|
||||||
});
|
|
||||||
|
|
||||||
predictionDrawnItems.addLayer(rectangle);
|
|
||||||
predictionRectangle = rectangle;
|
|
||||||
predictionMap.fitBounds(bounds);
|
|
||||||
updatePredictionBbox(L.latLngBounds(bounds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle prediction form submission
|
|
||||||
document.getElementById('predictionForm').onsubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
model_filename: document.getElementById('selectedModel').value,
|
|
||||||
min_lon: parseFloat(document.getElementById('predMinLon').value),
|
|
||||||
min_lat: parseFloat(document.getElementById('predMinLat').value),
|
|
||||||
max_lon: parseFloat(document.getElementById('predMaxLon').value),
|
|
||||||
max_lat: parseFloat(document.getElementById('predMaxLat').value),
|
|
||||||
start_date: document.getElementById('predStartDate').value,
|
|
||||||
end_date: document.getElementById('predEndDate').value,
|
|
||||||
max_scenes: parseInt(document.getElementById('predMaxScenes').value),
|
|
||||||
cloud_cover: parseInt(document.getElementById('predCloudCover').value),
|
|
||||||
resolution: parseInt(document.getElementById('predResolution').value)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!config.model_filename) {
|
|
||||||
alert('Vui lòng chọn model để dự đoán!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/prediction/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 prediction status
|
|
||||||
if (predictionStatusInterval) clearInterval(predictionStatusInterval);
|
|
||||||
predictionStatusInterval = setInterval(checkPredictionStatus, 2000);
|
|
||||||
|
|
||||||
document.getElementById('predictBtn').disabled = true;
|
|
||||||
document.getElementById('predictionResult').style.display = 'none';
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
alert('Lỗi kết nối: ' + error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check prediction status
|
|
||||||
async function checkPredictionStatus() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/prediction/status`);
|
|
||||||
const status = await response.json();
|
|
||||||
|
|
||||||
const statusBox = document.getElementById('predictionStatusBox');
|
|
||||||
const statusText = document.getElementById('predictionStatusText');
|
|
||||||
const progressText = document.getElementById('predictionProgressText');
|
|
||||||
|
|
||||||
statusText.textContent = status.is_predicting ? 'Đang dự đoán...' :
|
|
||||||
(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_predicting) {
|
|
||||||
statusBox.classList.add('training');
|
|
||||||
} else if (status.error) {
|
|
||||||
statusBox.classList.add('error');
|
|
||||||
} else if (status.result) {
|
|
||||||
statusBox.classList.add('success');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable/disable button
|
|
||||||
if (!status.is_predicting) {
|
|
||||||
document.getElementById('predictBtn').disabled = false;
|
|
||||||
|
|
||||||
if (predictionStatusInterval) {
|
|
||||||
clearInterval(predictionStatusInterval);
|
|
||||||
predictionStatusInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.result) {
|
|
||||||
displayPredictionResult(status.result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking prediction status:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display prediction result
|
|
||||||
function displayPredictionResult(result) {
|
|
||||||
const resultDiv = document.getElementById('predictionResult');
|
|
||||||
const resultText = document.getElementById('predResultText');
|
|
||||||
|
|
||||||
// Store result globally for download/view functions
|
|
||||||
window.lastPredictionResult = result;
|
|
||||||
|
|
||||||
// Extract filename from path
|
|
||||||
const filename = result.output_file.split('/').pop();
|
|
||||||
const downloadUrl = `${API_BASE}/predictions/download/${filename}`;
|
|
||||||
|
|
||||||
resultText.innerHTML = `
|
|
||||||
<div style="margin-bottom: 10px;">
|
|
||||||
<strong>📁 File kết quả:</strong><br>
|
|
||||||
<code style="background: #fff; padding: 5px 10px; border-radius: 4px; display: inline-block; margin-top: 5px;">${result.output_file}</code>
|
|
||||||
</div>
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px;">
|
|
||||||
<div><strong>📏 Kích thước:</strong> ${result.shape[0]} x ${result.shape[1]} pixels</div>
|
|
||||||
<div><strong>🎨 Các lớp:</strong> ${result.unique_classes.join(', ')}</div>
|
|
||||||
<div style="grid-column: 1 / -1;"><strong>📍 Khu vực:</strong> [${result.bbox.map(v => v.toFixed(4)).join(', ')}]</div>
|
|
||||||
<div style="grid-column: 1 / -1;"><strong>⏰ Thời gian:</strong> ${result.time_range}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add download link
|
|
||||||
const downloadContainer = document.getElementById('downloadLinkContainer');
|
|
||||||
downloadContainer.innerHTML = `
|
|
||||||
<a href="${downloadUrl}"
|
|
||||||
class="btn btn-primary"
|
|
||||||
style="background: #28a745; padding: 12px 24px; text-decoration: none; display: inline-block; margin-right: 10px;"
|
|
||||||
download="${filename}">
|
|
||||||
📥 Tải GeoTIFF
|
|
||||||
</a>
|
|
||||||
<button onclick="viewPredictionResult()" class="btn btn-secondary" style="background: #17a2b8;">
|
|
||||||
👁️ Xem Chi Tiết
|
|
||||||
</button>
|
|
||||||
<div style="margin-top: 10px; font-size: 12px; color: #666;">
|
|
||||||
Hoặc copy link: <a href="${downloadUrl}" target="_blank" style="color: #28a745;">${downloadUrl}</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
resultDiv.style.display = 'block';
|
|
||||||
|
|
||||||
// Refresh predictions list
|
|
||||||
loadPredictionsList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download prediction result
|
|
||||||
function downloadPredictionResult() {
|
|
||||||
if (window.lastPredictionResult) {
|
|
||||||
const result = window.lastPredictionResult;
|
|
||||||
const filename = result.output_file.split('/').pop();
|
|
||||||
const downloadUrl = `${API_BASE}/predictions/download/${filename}`;
|
|
||||||
window.open(downloadUrl, '_blank');
|
|
||||||
} else {
|
|
||||||
alert('Chưa có kết quả dự đoán nào!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load list of previous predictions
|
|
||||||
async function loadPredictionsList() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/predictions/list`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const listDiv = document.getElementById('predictionsList');
|
|
||||||
|
|
||||||
if (data.predictions && data.predictions.length > 0) {
|
|
||||||
listDiv.innerHTML = data.predictions.map(pred => `
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px; margin-bottom: 8px; background: white; border-radius: 6px; border: 1px solid #ddd;">
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<strong style="color: #333;">📄 ${pred.filename}</strong>
|
|
||||||
<div style="font-size: 12px; color: #666; margin-top: 3px;">
|
|
||||||
📅 ${new Date(pred.created).toLocaleString('vi-VN')} | 💾 ${pred.size_mb} MB
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="${pred.download_url}"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
style="background: #28a745; padding: 6px 12px; font-size: 12px; text-decoration: none;"
|
|
||||||
download="${pred.filename}">
|
|
||||||
📥 Tải về
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
} else {
|
|
||||||
listDiv.innerHTML = '<p style="color: #666; text-align: center;">Chưa có file dự đoán nào.</p>';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading predictions list:', error);
|
|
||||||
document.getElementById('predictionsList').innerHTML =
|
|
||||||
'<p style="color: #dc3545; text-align: center;">Lỗi tải danh sách: ' + error.message + '</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// View prediction result details
|
|
||||||
function viewPredictionResult() {
|
|
||||||
if (window.lastPredictionResult) {
|
|
||||||
const result = window.lastPredictionResult;
|
|
||||||
const details = `
|
|
||||||
=== CHI TIẾT KẾT QUẢ DỰ ĐOÁN ===
|
|
||||||
|
|
||||||
📁 File Output: ${result.output_file}
|
|
||||||
|
|
||||||
📊 Thông số ảnh:
|
|
||||||
- Kích thước: ${result.shape[0]} x ${result.shape[1]} pixels
|
|
||||||
- Tổng số pixels: ${result.shape[0] * result.shape[1]}
|
|
||||||
|
|
||||||
🎨 Phân loại:
|
|
||||||
- Các lớp tìm thấy: ${result.unique_classes.join(', ')}
|
|
||||||
- Số lớp phân biệt: ${result.unique_classes.length}
|
|
||||||
|
|
||||||
📍 Vị trí địa lý:
|
|
||||||
- Bbox: [${result.bbox.map(v => v.toFixed(6)).join(', ')}]
|
|
||||||
- Min Lon: ${result.bbox[0].toFixed(6)}°
|
|
||||||
- Min Lat: ${result.bbox[1].toFixed(6)}°
|
|
||||||
- Max Lon: ${result.bbox[2].toFixed(6)}°
|
|
||||||
- Max Lat: ${result.bbox[3].toFixed(6)}°
|
|
||||||
|
|
||||||
⏰ Khoảng thời gian: ${result.time_range}
|
|
||||||
|
|
||||||
✅ Trạng thái: Hoàn thành
|
|
||||||
`;
|
|
||||||
alert(details);
|
|
||||||
} else {
|
|
||||||
alert('Chưa có kết quả dự đoán nào!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user