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
|
||||
*.joblib
|
||||
*.nc
|
||||
@@ -13,6 +12,28 @@
|
||||
*.pb
|
||||
*.npz
|
||||
*.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
|
||||
# *.json
|
||||
@@ -33,3 +54,31 @@ dataset_cache/
|
||||
bfg.jar
|
||||
..bfg-report/
|
||||
.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/
|
||||
+603
-83
@@ -52,6 +52,10 @@ prediction_status = {
|
||||
"end_time": None
|
||||
}
|
||||
|
||||
# Batch prediction queue
|
||||
batch_queue = []
|
||||
batch_results = []
|
||||
|
||||
|
||||
class TrainingConfig(BaseModel):
|
||||
"""Cấu hình training"""
|
||||
@@ -120,23 +124,55 @@ class TrainingStatus(BaseModel):
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root():
|
||||
"""Serve giao diện web"""
|
||||
html_file = Path(__file__).parent / "training_interface.html"
|
||||
"""Serve main index page with tabs"""
|
||||
html_file = Path(__file__).parent / "index.html"
|
||||
if html_file.exists():
|
||||
return FileResponse(html_file)
|
||||
else:
|
||||
return HTMLResponse("""
|
||||
<html>
|
||||
<head><title>Training Interface</title></head>
|
||||
<head><title>Land Classification System</title></head>
|
||||
<body>
|
||||
<h1>Land Classification Training API</h1>
|
||||
<h1>Land Classification System</h1>
|
||||
<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>
|
||||
</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")
|
||||
async def get_presets():
|
||||
"""Lấy các preset cấu hình sẵn"""
|
||||
@@ -308,22 +344,37 @@ async def list_models():
|
||||
model_dir = Path("model_train")
|
||||
if not model_dir.exists():
|
||||
return {"models": []}
|
||||
|
||||
|
||||
models = []
|
||||
# List all .joblib model files (actual trained models)
|
||||
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 = {}
|
||||
# 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():
|
||||
with open(info_file) as f:
|
||||
info = json.load(f)
|
||||
try:
|
||||
with open(info_file) as 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({
|
||||
"filename": model_file.name,
|
||||
"created": datetime.fromtimestamp(model_file.stat().st_mtime).isoformat(),
|
||||
"size_mb": round(model_file.stat().st_size / 1024 / 1024, 2),
|
||||
"created": created,
|
||||
"size_mb": size_mb,
|
||||
"info": info
|
||||
})
|
||||
|
||||
|
||||
# Sort by creation time (newest first)
|
||||
models.sort(key=lambda x: x["created"], reverse=True)
|
||||
return {"models": models}
|
||||
@@ -550,6 +601,12 @@ async def run_prediction(config: PredictionConfig):
|
||||
import rioxarray
|
||||
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..."
|
||||
|
||||
# Load model
|
||||
@@ -577,47 +634,96 @@ async def run_prediction(config: PredictionConfig):
|
||||
except ImportError:
|
||||
raise ImportError("PyTorch is required for CNN prediction. Install: pip install torch")
|
||||
|
||||
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
||||
|
||||
# Import and use Microsoft Planetary Computer STAC API
|
||||
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,
|
||||
)
|
||||
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"
|
||||
|
||||
# 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}"
|
||||
|
||||
# ============ BƯỚC 1: 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(
|
||||
collections=["sentinel-2-l2a"],
|
||||
bbox=bbox,
|
||||
datetime=time_range,
|
||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||
)
|
||||
|
||||
s2_items = list(s2_search.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")
|
||||
|
||||
s2_items = s2_items[:config.max_scenes]
|
||||
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
||||
|
||||
# Load Sentinel-2 data
|
||||
s2_data = load(
|
||||
s2_items,
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="solar_day",
|
||||
resolution=config.resolution
|
||||
)
|
||||
|
||||
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 planetary_computer
|
||||
from odc.stac import load
|
||||
catalog = pystac_client.Client.open(
|
||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||
modifier=planetary_computer.sign_inplace,
|
||||
)
|
||||
# ============ BƯỚC 1: TẢI DỮ LIỆU SENTINEL-2 ============
|
||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
||||
s2_search = catalog.search(
|
||||
collections=["sentinel-2-l2a"],
|
||||
bbox=bbox,
|
||||
datetime=time_range,
|
||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||
)
|
||||
s2_items = list(s2_search.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")
|
||||
s2_items = s2_items[:config.max_scenes]
|
||||
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
||||
s2_data = load(
|
||||
s2_items,
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="solar_day",
|
||||
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 ============
|
||||
prediction_status["progress"] = "Đang tính toán NDVI và xử lý mây..."
|
||||
@@ -649,47 +755,63 @@ async def run_prediction(config: PredictionConfig):
|
||||
ndvi_monthly = ndvi_monthly.compute()
|
||||
|
||||
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (VH, VV) ============
|
||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
||||
|
||||
# Search Sentinel-1 data
|
||||
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..."
|
||||
# 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)..."
|
||||
|
||||
# Load Sentinel-1 data (without like= to avoid conflict with bbox/resolution)
|
||||
s1_data = load(
|
||||
s1_items,
|
||||
# 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
|
||||
s1_search = catalog.search(
|
||||
collections=["sentinel-1-rtc"],
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="sat:absolute_orbit",
|
||||
resolution=config.resolution
|
||||
datetime=time_range,
|
||||
)
|
||||
|
||||
# Extract VH and VV bands
|
||||
if "vh" in s1_data and "vv" in s1_data:
|
||||
vh = s1_data["vh"].astype('float32')
|
||||
vv = s1_data["vv"].astype('float32')
|
||||
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..."
|
||||
|
||||
# Resample to monthly average
|
||||
prediction_status["progress"] = "Đang tính trung bình VH/VV theo tháng..."
|
||||
vh_monthly = vh.resample(time="1ME").mean().compute()
|
||||
vv_monthly = vv.resample(time="1ME").mean().compute()
|
||||
# Load Sentinel-1 data (without like= to avoid conflict with bbox/resolution)
|
||||
s1_data = load(
|
||||
s1_items,
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="sat:absolute_orbit",
|
||||
resolution=config.resolution
|
||||
)
|
||||
|
||||
use_radar = True
|
||||
# Extract VH and VV bands
|
||||
if "vh" in s1_data and "vv" in s1_data:
|
||||
vh = s1_data["vh"].astype('float32')
|
||||
vv = s1_data["vv"].astype('float32')
|
||||
|
||||
# Resample to monthly average
|
||||
prediction_status["progress"] = "Đang tính trung bình VH/VV theo tháng..."
|
||||
vh_monthly = vh.resample(time="1ME").mean().compute()
|
||||
vv_monthly = vv.resample(time="1ME").mean().compute()
|
||||
|
||||
use_radar = True
|
||||
else:
|
||||
prediction_status["progress"] = "Không tìm thấy bands VH/VV, tiếp tục với NDVI..."
|
||||
use_radar = False
|
||||
else:
|
||||
prediction_status["progress"] = "Không tìm thấy bands VH/VV, tiếp tục với NDVI..."
|
||||
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, tiếp tục với NDVI..."
|
||||
use_radar = False
|
||||
else:
|
||||
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, tiếp tục với NDVI..."
|
||||
use_radar = False
|
||||
|
||||
# ============ BƯỚC 5: CHUẨN BỊ FEATURES CHO DỰ ĐOÁN ============
|
||||
prediction_status["progress"] = "Đang chuẩn bị features cho dự đoán..."
|
||||
@@ -819,6 +941,40 @@ async def run_prediction(config: PredictionConfig):
|
||||
|
||||
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
|
||||
unique_classes = np.unique(predictions_2d)
|
||||
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["result"] = {
|
||||
"output_file": str(output_file),
|
||||
"png_file": str(png_file) if png_file else None,
|
||||
"shape": list(pred_shape),
|
||||
"unique_classes": unique_classes,
|
||||
"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__":
|
||||
print("=" * 70)
|
||||
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()
|
||||
+9
-439
@@ -286,7 +286,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -507,6 +507,10 @@
|
||||
<div id="modelsList" class="model-list" style="margin-top: 15px;">
|
||||
<p>Đang tải...</p>
|
||||
</div>
|
||||
<div style="margin-top: 15px; visibility: hidden;">
|
||||
<label for="selectedModel" style="font-weight:600; color:#667eea;">Chọn model để dự đoán:</label>
|
||||
<select id="selectedModel" style="width:100%; padding:10px; border-radius:5px; font-size:1em; margin-top:5px;"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reports Section -->
|
||||
@@ -518,114 +522,7 @@
|
||||
</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>
|
||||
|
||||
@@ -644,15 +541,15 @@
|
||||
times: []
|
||||
};
|
||||
|
||||
// Load presets on page load
|
||||
window.onload = async () => {
|
||||
// Load presets and models only after DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadPresets();
|
||||
await loadModels();
|
||||
await loadReports();
|
||||
await loadSystemInfo();
|
||||
checkStatus();
|
||||
loadTrainingHistory();
|
||||
};
|
||||
});
|
||||
|
||||
// Load preset configurations
|
||||
async function loadPresets() {
|
||||
@@ -1387,333 +1284,6 @@
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user