hoàn thành cơ bản các chức năng
This commit is contained in:
Binary file not shown.
Binary file not shown.
+184
-4
@@ -68,11 +68,15 @@ class TrainingConfig(BaseModel):
|
||||
resolution: int = 20 # 10m hoặc 20m
|
||||
|
||||
# Model parameters
|
||||
model_type: str = "xgboost" # xgboost, random_forest, decision_tree, svm, cnn
|
||||
n_estimators: int = 100
|
||||
max_depth: int = 20
|
||||
learning_rate: float = 0.1
|
||||
use_gpu: bool = True
|
||||
|
||||
# Cache
|
||||
use_cache: bool = True # Cache dataset để test nhanh hơn
|
||||
|
||||
# Training data
|
||||
training_shapefile: str = "train/ST_training data_updated_1130points_new.shp"
|
||||
|
||||
@@ -205,6 +209,90 @@ async def stop_training():
|
||||
return {"message": "Đang dừng training..."}
|
||||
|
||||
|
||||
@app.post("/api/cache/clear")
|
||||
async def clear_cache():
|
||||
"""Xóa cache dataset"""
|
||||
import shutil
|
||||
cache_dir = Path("dataset_cache")
|
||||
|
||||
if not cache_dir.exists():
|
||||
return {"message": "Không có cache để xóa", "deleted": 0}
|
||||
|
||||
# Count files
|
||||
cache_files = list(cache_dir.glob("*.joblib"))
|
||||
count = len(cache_files)
|
||||
|
||||
# Delete all cache files
|
||||
for cache_file in cache_files:
|
||||
try:
|
||||
cache_file.unlink()
|
||||
except:
|
||||
pass
|
||||
|
||||
return {"message": f"Đã xóa {count} file cache", "deleted": count}
|
||||
|
||||
|
||||
@app.get("/api/cache/info")
|
||||
async def get_cache_info():
|
||||
"""Lấy thông tin về cache với metadata đầy đủ"""
|
||||
cache_dir = Path("dataset_cache")
|
||||
|
||||
if not cache_dir.exists():
|
||||
return {"exists": False, "files": [], "total_size_mb": 0}
|
||||
|
||||
cache_files = []
|
||||
total_size = 0
|
||||
|
||||
for cache_file in cache_dir.glob("*.joblib"):
|
||||
size = cache_file.stat().st_size
|
||||
total_size += size
|
||||
|
||||
# Try to load metadata from cache
|
||||
metadata = {}
|
||||
try:
|
||||
cached_data = joblib.load(cache_file)
|
||||
if isinstance(cached_data, dict):
|
||||
metadata = {
|
||||
"bbox": cached_data.get("bbox", []),
|
||||
"time_range": cached_data.get("time_range", ""),
|
||||
"resolution": cached_data.get("resolution", 20),
|
||||
"n_samples": len(cached_data.get("features", [])),
|
||||
"created": cached_data.get("timestamp", "")
|
||||
}
|
||||
# Parse time_range to get start/end dates
|
||||
if metadata["time_range"]:
|
||||
time_parts = metadata["time_range"].split("/")
|
||||
if len(time_parts) == 2:
|
||||
metadata["start_date"] = time_parts[0]
|
||||
metadata["end_date"] = time_parts[1]
|
||||
# Parse bbox to get min/max lon/lat
|
||||
if metadata["bbox"] and len(metadata["bbox"]) == 4:
|
||||
metadata["min_lon"] = metadata["bbox"][0]
|
||||
metadata["min_lat"] = metadata["bbox"][1]
|
||||
metadata["max_lon"] = metadata["bbox"][2]
|
||||
metadata["max_lat"] = metadata["bbox"][3]
|
||||
except Exception as e:
|
||||
print(f"Error loading cache metadata: {e}")
|
||||
|
||||
cache_files.append({
|
||||
"filename": cache_file.name,
|
||||
"size_mb": round(size / 1024 / 1024, 2),
|
||||
"modified": datetime.fromtimestamp(cache_file.stat().st_mtime).isoformat(),
|
||||
"metadata": metadata
|
||||
})
|
||||
|
||||
# Sort by modified time (newest first)
|
||||
cache_files.sort(key=lambda x: x["modified"], reverse=True)
|
||||
|
||||
return {
|
||||
"exists": True,
|
||||
"files": cache_files,
|
||||
"count": len(cache_files),
|
||||
"total_size_mb": round(total_size / 1024 / 1024, 2)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@app.get("/api/models/list")
|
||||
async def list_models():
|
||||
"""Liệt kê các model đã train"""
|
||||
@@ -288,10 +376,12 @@ async def run_training(config: TrainingConfig):
|
||||
cloud_cover=config.cloud_cover,
|
||||
resolution=config.resolution,
|
||||
training_shapefile=config.training_shapefile,
|
||||
model_type=config.model_type,
|
||||
n_estimators=config.n_estimators,
|
||||
max_depth=config.max_depth,
|
||||
learning_rate=config.learning_rate,
|
||||
use_gpu=config.use_gpu,
|
||||
use_cache=config.use_cache,
|
||||
status_callback=lambda msg: update_progress(msg),
|
||||
cancel_check=should_cancel
|
||||
)
|
||||
@@ -360,6 +450,16 @@ async def run_prediction(config: PredictionConfig):
|
||||
model = model_data
|
||||
label_encoder = None
|
||||
|
||||
# Check if it's a CNN model (PyTorch)
|
||||
is_cnn_model = hasattr(model, '__class__') and 'CNN' in model.__class__.__name__
|
||||
if is_cnn_model:
|
||||
prediction_status["progress"] = "Phát hiện PyTorch CNN model..."
|
||||
# Import PyTorch if needed
|
||||
try:
|
||||
import torch
|
||||
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
|
||||
@@ -447,14 +547,13 @@ async def run_prediction(config: PredictionConfig):
|
||||
s1_items = s1_items[:config.max_scenes]
|
||||
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
|
||||
|
||||
# Load Sentinel-1 data
|
||||
# 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,
|
||||
like=ndvi_monthly # Align with NDVI grid
|
||||
resolution=config.resolution
|
||||
)
|
||||
|
||||
# Extract VH and VV bands
|
||||
@@ -514,13 +613,51 @@ async def run_prediction(config: PredictionConfig):
|
||||
vv_t = np.resize(vv_t, n_pixels)
|
||||
features = np.column_stack([features, vv_t])
|
||||
|
||||
# Handle NaN values in features
|
||||
# Handle NaN values in features✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch
|
||||
features = np.nan_to_num(features, nan=0.0)
|
||||
|
||||
# ============ BƯỚC 6: DỰ ĐOÁN ============
|
||||
# Check model's expected feature count and adjust
|
||||
try:
|
||||
# Get expected number of features from model
|
||||
if is_cnn_model:
|
||||
# For PyTorch CNN, get n_features from model
|
||||
expected_features = model.n_features
|
||||
elif hasattr(model, 'n_features_in_'):
|
||||
expected_features = model.n_features_in_
|
||||
elif hasattr(model, 'feature_names_in_'):
|
||||
expected_features = len(model.feature_names_in_)
|
||||
else:
|
||||
# Try to get from booster for XGBoost
|
||||
try:
|
||||
expected_features = model.get_booster().num_features()
|
||||
except:
|
||||
expected_features = features.shape[1]
|
||||
|
||||
prediction_status["progress"] = f"Model cần {expected_features} features, đang có {features.shape[1]} features..."
|
||||
|
||||
# Adjust features to match model
|
||||
if features.shape[1] > expected_features:
|
||||
# Trim to expected number (use only first N features - NDVI only)
|
||||
prediction_status["progress"] = f"Cắt bớt features từ {features.shape[1]} xuống {expected_features}..."
|
||||
features = features[:, :expected_features]
|
||||
elif features.shape[1] < expected_features:
|
||||
# Pad with zeros or repeat last features
|
||||
prediction_status["progress"] = f"Thêm features từ {features.shape[1]} lên {expected_features}..."
|
||||
n_missing = expected_features - features.shape[1]
|
||||
# Repeat last feature column to fill
|
||||
padding = np.tile(features[:, -1:], (1, n_missing))
|
||||
features = np.column_stack([features, padding])
|
||||
except Exception as e:
|
||||
prediction_status["progress"] = f"Không thể xác định số features của model, tiếp tục với {features.shape[1]} features..."
|
||||
|
||||
prediction_status["progress"] = f"Đang dự đoán với {features.shape[1]} features..."
|
||||
|
||||
# Make prediction
|
||||
if is_cnn_model:
|
||||
# PyTorch CNN prediction
|
||||
predictions = model.predict(features)
|
||||
else:
|
||||
predictions = model.predict(features)
|
||||
|
||||
# Decode labels if label_encoder exists
|
||||
@@ -594,6 +731,49 @@ async def run_prediction(config: PredictionConfig):
|
||||
print(traceback.format_exc())
|
||||
|
||||
|
||||
@app.get("/api/predictions/list")
|
||||
async def list_predictions():
|
||||
"""Lấy danh sách các file prediction đã tạo"""
|
||||
predictions_dir = Path("predictions")
|
||||
predictions_dir.mkdir(exist_ok=True)
|
||||
|
||||
predictions = []
|
||||
for pred_file in predictions_dir.glob("*.tif"):
|
||||
predictions.append({
|
||||
"filename": pred_file.name,
|
||||
"created": datetime.fromtimestamp(pred_file.stat().st_mtime).isoformat(),
|
||||
"size_mb": round(pred_file.stat().st_size / 1024 / 1024, 2),
|
||||
"download_url": f"/api/predictions/download/{pred_file.name}"
|
||||
})
|
||||
|
||||
# Sort by creation time (newest first)
|
||||
predictions.sort(key=lambda x: x["created"], reverse=True)
|
||||
return {"predictions": predictions}
|
||||
|
||||
|
||||
@app.get("/api/predictions/download/{filename}")
|
||||
async def download_prediction(filename: str):
|
||||
"""Download file prediction GeoTIFF"""
|
||||
predictions_dir = Path("predictions")
|
||||
file_path = predictions_dir / filename
|
||||
|
||||
# Security check: ensure filename doesn't contain path traversal
|
||||
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"File không tồn tại: {filename}")
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
filename=filename,
|
||||
media_type="image/tiff",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 70)
|
||||
print("🚀 LAND CLASSIFICATION TRAINING API SERVER")
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7318566231680cb0c97883b7a5e4177aa1bfeec462a0f52ed34aa103ddec210b
|
||||
size 20903
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9cabf5e0241dcc3a73133ac8ae11171f34042c491f895b268c016407619bdfe1
|
||||
size 20903
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae2f82f6c837396729cc63efa41ee3048d9a7de3197e28318dc846be830239b9
|
||||
size 41536
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T18:04:27.406540",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 0.515686274509804,
|
||||
"test_accuracy": 0.5,
|
||||
"model_type": "cnn",
|
||||
"device": "cpu",
|
||||
"n_estimators": 50,
|
||||
"max_depth": null,
|
||||
"learning_rate": null,
|
||||
"cnn_epochs": 25,
|
||||
"n_features": 3,
|
||||
"n_classes": 7,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:abc03d62c7f620b88150a6481143026d516fd3a34bd5ab67c734adac4a8900f9
|
||||
size 41536
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T18:17:48.399298",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 0.4803921568627451,
|
||||
"test_accuracy": 0.484375,
|
||||
"model_type": "cnn",
|
||||
"device": "cpu",
|
||||
"n_estimators": 50,
|
||||
"max_depth": null,
|
||||
"learning_rate": null,
|
||||
"cnn_epochs": 25,
|
||||
"n_features": 3,
|
||||
"n_classes": 7,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-25",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:21f906aa2a61d2e793a3463df95fe3e364134934af326771efae90d80caca419
|
||||
size 41536
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T18:23:10.713912",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 0.46862745098039216,
|
||||
"test_accuracy": 0.4609375,
|
||||
"model_type": "cnn",
|
||||
"device": "cpu",
|
||||
"n_estimators": 50,
|
||||
"max_depth": null,
|
||||
"learning_rate": null,
|
||||
"cnn_epochs": 25,
|
||||
"n_features": 3,
|
||||
"n_classes": 7,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b717f564f9413a6e5c9cd3f7011cbc18be02479691d01c554986defb400f0490
|
||||
size 1347520
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T13:40:03.325930",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 1.0,
|
||||
"test_accuracy": 0.578125,
|
||||
"model_type": "XGBClassifier",
|
||||
"device": "cuda:0",
|
||||
"tree_method": "hist",
|
||||
"n_estimators": 100,
|
||||
"max_depth": 20,
|
||||
"learning_rate": 0.1,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a44efc173619782c8add224e9024f33ef0150ab192b5435d009cb119c379f03c
|
||||
size 2088632
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T16:52:19.862770",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 859,
|
||||
"testing_samples": 215,
|
||||
"train_accuracy": 0.9976717112922002,
|
||||
"test_accuracy": 0.6837209302325581,
|
||||
"model_type": "XGBClassifier",
|
||||
"device": "cuda:0",
|
||||
"tree_method": "hist",
|
||||
"n_estimators": 100,
|
||||
"max_depth": 20,
|
||||
"learning_rate": 0.1,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-12-31",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||
size 73272920
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8fb13c2e466b9811104cbd7747ea6ca8c14ddb44802d934aadfe52a4b5dd916
|
||||
size 73272920
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a2fe092e1fd96e56da519acffe5ca5c9a8246e796bc5700cffb9694dc99f3aec
|
||||
size 73272920
|
||||
+246
-16
@@ -9,11 +9,109 @@ import geopandas as gpd
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.preprocessing import LabelEncoder
|
||||
from sklearn.metrics import classification_report, confusion_matrix
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.tree import DecisionTreeClassifier
|
||||
from sklearn.svm import SVC
|
||||
from xgboost import XGBClassifier
|
||||
import joblib
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import warnings
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# PyTorch for CNN
|
||||
try:
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import TensorDataset, DataLoader
|
||||
PYTORCH_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYTORCH_AVAILABLE = False
|
||||
print("Warning: PyTorch not available. CNN model will not work.")
|
||||
|
||||
# Define CNN model class for PyTorch
|
||||
class CNNClassifier(nn.Module):
|
||||
def __init__(self, n_features, n_classes):
|
||||
super(CNNClassifier, self).__init__()
|
||||
self.n_features = n_features
|
||||
self.n_classes = n_classes
|
||||
|
||||
# For small feature sets (like 3 features), use simpler architecture
|
||||
if n_features < 8:
|
||||
# Simple fully connected network for small features
|
||||
self.use_conv = False
|
||||
self.fc1 = nn.Linear(n_features, 64)
|
||||
self.dropout1 = nn.Dropout(0.3)
|
||||
self.fc2 = nn.Linear(64, 128)
|
||||
self.dropout2 = nn.Dropout(0.5)
|
||||
self.fc3 = nn.Linear(128, n_classes)
|
||||
else:
|
||||
# CNN architecture for larger feature sets
|
||||
self.use_conv = True
|
||||
self.conv1 = nn.Conv1d(in_channels=1, out_channels=32, kernel_size=3, padding=1)
|
||||
self.pool1 = nn.MaxPool1d(kernel_size=2)
|
||||
self.conv2 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1)
|
||||
self.pool2 = nn.MaxPool1d(kernel_size=2)
|
||||
|
||||
# Calculate size after convolutions
|
||||
conv_output_size = (n_features // 2 // 2) * 64
|
||||
|
||||
# Fully connected layers
|
||||
self.fc1 = nn.Linear(conv_output_size, 128)
|
||||
self.dropout = nn.Dropout(0.5)
|
||||
self.fc2 = nn.Linear(128, n_classes)
|
||||
|
||||
def forward(self, x):
|
||||
# x shape: (batch, n_features) or (batch, 1, n_features)
|
||||
if self.use_conv:
|
||||
# CNN path for larger feature sets
|
||||
if len(x.shape) == 2:
|
||||
x = x.unsqueeze(1) # Add channel dimension
|
||||
x = F.relu(self.conv1(x))
|
||||
x = self.pool1(x)
|
||||
x = F.relu(self.conv2(x))
|
||||
x = self.pool2(x)
|
||||
x = x.view(x.size(0), -1) # Flatten
|
||||
x = F.relu(self.fc1(x))
|
||||
x = self.dropout(x)
|
||||
x = self.fc2(x)
|
||||
else:
|
||||
# Fully connected path for small feature sets
|
||||
if len(x.shape) == 3:
|
||||
x = x.squeeze(1) # Remove channel dimension if present
|
||||
x = F.relu(self.fc1(x))
|
||||
x = self.dropout1(x)
|
||||
x = F.relu(self.fc2(x))
|
||||
x = self.dropout2(x)
|
||||
x = self.fc3(x)
|
||||
return x
|
||||
|
||||
def predict(self, X):
|
||||
"""Scikit-learn style predict method"""
|
||||
self.eval()
|
||||
with torch.no_grad():
|
||||
if isinstance(X, np.ndarray):
|
||||
X = torch.FloatTensor(X)
|
||||
# Handle both 2D and 3D inputs
|
||||
if not self.use_conv and len(X.shape) == 3:
|
||||
X = X.squeeze(1)
|
||||
elif self.use_conv and len(X.shape) == 2:
|
||||
X = X.unsqueeze(1)
|
||||
outputs = self(X)
|
||||
_, predicted = torch.max(outputs, 1)
|
||||
return predicted.cpu().numpy()
|
||||
|
||||
def score(self, X, y):
|
||||
"""Scikit-learn style score method"""
|
||||
predictions = self.predict(X)
|
||||
if isinstance(y, torch.Tensor):
|
||||
y = y.cpu().numpy()
|
||||
return np.mean(predictions == y)
|
||||
|
||||
# Microsoft Planetary Computer imports
|
||||
import planetary_computer
|
||||
@@ -28,10 +126,12 @@ def train_model(
|
||||
cloud_cover=30,
|
||||
resolution=20,
|
||||
training_shapefile='train/ST_training data_updated_1130points_new.shp',
|
||||
model_type='xgboost',
|
||||
n_estimators=100,
|
||||
max_depth=20,
|
||||
learning_rate=0.1,
|
||||
use_gpu=True,
|
||||
use_cache=True,
|
||||
output_model_path=None,
|
||||
status_callback=None,
|
||||
cancel_check=None
|
||||
@@ -77,10 +177,39 @@ def train_model(
|
||||
# Auto-generate output path if not provided
|
||||
if output_model_path is None:
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
output_model_path = f'model_train/model_xgboost_gpu_{timestamp}.joblib'
|
||||
output_model_path = f'model_train/model_{model_type}_{timestamp}.joblib'
|
||||
|
||||
# ============ CACHE SYSTEM ============
|
||||
# Create cache directory
|
||||
cache_dir = Path("dataset_cache")
|
||||
cache_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Generate cache key from parameters
|
||||
cache_params = f"{bbox}_{time_range}_{max_scenes}_{cloud_cover}_{resolution}"
|
||||
cache_key = hashlib.md5(cache_params.encode()).hexdigest()
|
||||
cache_file = cache_dir / f"training_data_{cache_key}.joblib"
|
||||
|
||||
features = None
|
||||
labels = None
|
||||
|
||||
# Try to load from cache
|
||||
if use_cache and cache_file.exists():
|
||||
update_status(f"📦 Loading cached dataset from {cache_file.name}...", 5)
|
||||
try:
|
||||
cached_data = joblib.load(cache_file)
|
||||
features = cached_data['features']
|
||||
labels = cached_data['labels']
|
||||
update_status(f"✅ Loaded {len(features)} samples from cache (skipped satellite download!)", 50)
|
||||
except Exception as e:
|
||||
update_status(f"⚠️ Cache load failed: {str(e)}, downloading fresh data...", 10)
|
||||
features = None
|
||||
|
||||
# If no cache or cache failed, download data
|
||||
if features is None:
|
||||
update_status("📡 Cache not found or disabled, downloading satellite data...", 10)
|
||||
|
||||
# Connect to Microsoft Planetary Computer
|
||||
update_status("Connecting to Microsoft Planetary Computer...", 0)
|
||||
update_status("Connecting to Microsoft Planetary Computer...", 12)
|
||||
catalog = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1")
|
||||
check_cancellation()
|
||||
|
||||
@@ -211,6 +340,23 @@ def train_model(
|
||||
|
||||
update_status(f"Extracted {len(features)} valid training samples", 70)
|
||||
|
||||
# ============ SAVE TO CACHE ============
|
||||
if use_cache:
|
||||
update_status(f"💾 Saving dataset to cache for future use...", 72)
|
||||
try:
|
||||
cache_data = {
|
||||
'features': features,
|
||||
'labels': labels,
|
||||
'bbox': bbox,
|
||||
'time_range': time_range,
|
||||
'resolution': resolution,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
joblib.dump(cache_data, cache_file)
|
||||
update_status(f"✅ Cached to {cache_file.name}", 75)
|
||||
except Exception as e:
|
||||
update_status(f"⚠️ Cache save failed: {str(e)}", 75)
|
||||
|
||||
# Encode labels
|
||||
label_encoder = LabelEncoder()
|
||||
labels_encoded = label_encoder.fit_transform(labels)
|
||||
@@ -220,33 +366,115 @@ def train_model(
|
||||
features, labels_encoded, test_size=0.2, random_state=42, stratify=labels_encoded
|
||||
)
|
||||
|
||||
# Train XGBoost model
|
||||
update_status("Training XGBoost model on GPU...", 75)
|
||||
# Train model based on selected type
|
||||
update_status(f"Training {model_type.upper()} model...", 75)
|
||||
|
||||
device = 'cuda:0' if use_gpu else 'cpu'
|
||||
|
||||
xgb_model = XGBClassifier(
|
||||
if model_type == 'xgboost':
|
||||
model = XGBClassifier(
|
||||
n_estimators=n_estimators,
|
||||
max_depth=max_depth,
|
||||
learning_rate=learning_rate,
|
||||
device=device,
|
||||
device=device if use_gpu else 'cpu',
|
||||
tree_method='hist',
|
||||
random_state=42,
|
||||
eval_metric='mlogloss',
|
||||
verbosity=0
|
||||
)
|
||||
elif model_type == 'random_forest':
|
||||
model = RandomForestClassifier(
|
||||
n_estimators=n_estimators,
|
||||
max_depth=max_depth,
|
||||
random_state=42,
|
||||
n_jobs=-1, # Use all cores
|
||||
verbose=0
|
||||
)
|
||||
elif model_type == 'decision_tree':
|
||||
model = DecisionTreeClassifier(
|
||||
max_depth=max_depth,
|
||||
random_state=42
|
||||
)
|
||||
elif model_type == 'svm':
|
||||
model = SVC(
|
||||
kernel='rbf',
|
||||
random_state=42,
|
||||
verbose=False
|
||||
)
|
||||
elif model_type == 'cnn':
|
||||
if not PYTORCH_AVAILABLE:
|
||||
raise ImportError("PyTorch is required for CNN. Install: pip install torch")
|
||||
|
||||
xgb_model.fit(X_train, y_train)
|
||||
# CNN requires reshaping data
|
||||
n_features = X_train.shape[1]
|
||||
n_classes = len(np.unique(y_train))
|
||||
|
||||
# Build PyTorch CNN model
|
||||
device = torch.device('cuda' if torch.cuda.is_available() and use_gpu else 'cpu')
|
||||
update_status(f"Building CNN model on {device}...", 75)
|
||||
|
||||
model = CNNClassifier(n_features, n_classes).to(device)
|
||||
|
||||
# Convert to PyTorch tensors
|
||||
X_train_tensor = torch.FloatTensor(X_train).unsqueeze(1) # Add channel dim: (N, 1, features)
|
||||
y_train_tensor = torch.LongTensor(y_train)
|
||||
X_test_tensor = torch.FloatTensor(X_test).unsqueeze(1)
|
||||
y_test_tensor = torch.LongTensor(y_test)
|
||||
|
||||
# Create data loaders
|
||||
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
|
||||
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
|
||||
|
||||
# Loss and optimizer
|
||||
criterion = nn.CrossEntropyLoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=0.001)
|
||||
|
||||
# Train CNN
|
||||
update_status("Training CNN model with PyTorch...", 80)
|
||||
epochs = min(50, n_estimators // 2) # Use n_estimators as epochs
|
||||
|
||||
model.train()
|
||||
for epoch in range(epochs):
|
||||
epoch_loss = 0.0
|
||||
for batch_X, batch_y in train_loader:
|
||||
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
|
||||
|
||||
optimizer.zero_grad()
|
||||
outputs = model(batch_X)
|
||||
loss = criterion(outputs, batch_y)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
epoch_loss += loss.item()
|
||||
|
||||
if (epoch + 1) % 10 == 0:
|
||||
avg_loss = epoch_loss / len(train_loader)
|
||||
update_status(f"CNN Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}", 80 + (epoch / epochs) * 10)
|
||||
|
||||
# Move model to CPU for saving (compatible with non-GPU systems)
|
||||
model = model.cpu()
|
||||
model.device_used = str(device)
|
||||
else:
|
||||
raise ValueError(f"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn")
|
||||
|
||||
# Fit non-CNN models
|
||||
if model_type != 'cnn':
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Evaluate
|
||||
update_status("Evaluating model...", 90)
|
||||
train_score = xgb_model.score(X_train, y_train)
|
||||
test_score = xgb_model.score(X_test, y_test)
|
||||
if model_type == 'cnn':
|
||||
# PyTorch CNN evaluation
|
||||
train_score = model.score(X_train, y_train)
|
||||
test_score = model.score(X_test, y_test)
|
||||
else:
|
||||
train_score = model.score(X_train, y_train)
|
||||
test_score = model.score(X_test, y_test)
|
||||
|
||||
# Save model
|
||||
update_status("Saving model...", 95)
|
||||
os.makedirs(os.path.dirname(output_model_path), exist_ok=True)
|
||||
joblib.dump({'model': xgb_model, 'label_encoder': label_encoder}, output_model_path)
|
||||
joblib.dump({'model': model, 'label_encoder': label_encoder}, output_model_path)
|
||||
|
||||
# Save model info
|
||||
info = {
|
||||
@@ -258,12 +486,14 @@ def train_model(
|
||||
"testing_samples": len(X_test),
|
||||
"train_accuracy": float(train_score),
|
||||
"test_accuracy": float(test_score),
|
||||
"model_type": "XGBClassifier",
|
||||
"device": device,
|
||||
"tree_method": "hist",
|
||||
"n_estimators": n_estimators,
|
||||
"max_depth": max_depth,
|
||||
"learning_rate": learning_rate,
|
||||
"model_type": model_type,
|
||||
"device": device if model_type == 'xgboost' else 'cpu',
|
||||
"n_estimators": n_estimators if model_type in ['xgboost', 'random_forest', 'cnn'] else None,
|
||||
"max_depth": max_depth if model_type != 'cnn' else None,
|
||||
"learning_rate": learning_rate if model_type == 'xgboost' else None,
|
||||
"cnn_epochs": min(50, n_estimators // 2) if model_type == 'cnn' else None,
|
||||
"n_features": X_train.shape[1],
|
||||
"n_classes": len(np.unique(y_train)),
|
||||
"bbox": bbox,
|
||||
"time_range": time_range,
|
||||
"resolution": resolution
|
||||
|
||||
+349
-13
@@ -258,6 +258,29 @@
|
||||
margin: 5px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Animations for notifications */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -369,7 +392,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 20px 0 15px; color: #667eea;">🛰️ Dữ Liệu Vệ Tinh</h3>
|
||||
<h3 style="margin: 20px 0 15px; color: #667eea;">� Dataset Cache Preset</h3>
|
||||
<div class="form-group">
|
||||
<label>Chọn Dataset đã cache:</label>
|
||||
<select id="cachePreset" style="font-size: 14px;">
|
||||
<option value="">-- Không dùng cache preset --</option>
|
||||
</select>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||||
💡 Chọn dataset cache có sẵn để tự động điền các thông số tương ứng
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 20px 0 15px; color: #667eea;">�🛰️ Dữ Liệu Vệ Tinh</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Số scenes tối đa:</label>
|
||||
@@ -390,8 +424,27 @@
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 20px 0 15px; color: #667eea;">🤖 Model Parameters</h3>
|
||||
<div class="form-row">
|
||||
|
||||
<!-- Model Type Selection -->
|
||||
<div class="form-group">
|
||||
<label><strong>🧠 Loại Model:</strong></label>
|
||||
<select id="modelType" required style="font-weight: 600; font-size: 14px;">
|
||||
<option value="xgboost" selected>🚀 XGBoost (Nhanh, Chính xác cao, Hỗ trợ GPU)</option>
|
||||
<option value="random_forest">🌲 Random Forest (Ổn định, Không cần GPU)</option>
|
||||
<option value="decision_tree">🌳 Decision Tree (Đơn giản, Nhanh nhất)</option>
|
||||
<option value="svm">🎯 SVM (Chính xác, Chậm với dữ liệu lớn)</option>
|
||||
<option value="cnn">🧠 CNN - Deep Learning (PyTorch, Tốt với ảnh vệ tinh, Hỗ trợ GPU)</option>
|
||||
</select>
|
||||
<div style="margin-top: 8px; padding: 10px; background: #e7f3ff; border-radius: 5px; font-size: 12px;">
|
||||
<span id="modelTypeDesc" style="color: #1976d2;">
|
||||
✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model-specific parameters -->
|
||||
<div class="form-row">
|
||||
<div class="form-group" id="nEstimatorsGroup">
|
||||
<label>N Estimators:</label>
|
||||
<input type="number" id="nEstimators" value="100" min="10" max="1000" required>
|
||||
</div>
|
||||
@@ -399,11 +452,11 @@
|
||||
<label>Max Depth:</label>
|
||||
<input type="number" id="maxDepth" value="20" min="1" max="50" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="learningRateGroup">
|
||||
<label>Learning Rate:</label>
|
||||
<input type="number" step="0.01" id="learningRate" value="0.1" min="0.01" max="1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="useGpuGroup">
|
||||
<label>Use GPU:</label>
|
||||
<select id="useGpu" required>
|
||||
<option value="true" selected>Có (RTX 4060)</option>
|
||||
@@ -412,6 +465,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cache Option -->
|
||||
<div class="form-group" style="margin-top: 20px; padding: 15px; background: #fff8dc; border-radius: 8px; border: 1px solid #ffc107;">
|
||||
<label style="display: flex; align-items: center; cursor: pointer; margin: 0;">
|
||||
<input type="checkbox" id="useCache" checked style="width: 18px; height: 18px; margin-right: 10px;">
|
||||
<span style="font-weight: 600; color: #856404;">💾 Sử dụng Cache Dataset</span>
|
||||
</label>
|
||||
<div style="font-size: 12px; color: #856404; margin-top: 8px; margin-left: 28px;">
|
||||
✅ <strong>Khuyến nghị:</strong> Bật để test nhanh hơn. Lần đầu load dữ liệu sẽ chậm, nhưng các lần sau rất nhanh (không cần download lại từ satellite).<br>
|
||||
📊 <span id="cacheInfo">Đang kiểm tra cache...</span>
|
||||
</div>
|
||||
<div style="margin-top: 10px; margin-left: 28px;">
|
||||
<button type="button" class="btn btn-secondary" onclick="clearCache()" style="font-size: 12px; padding: 5px 10px; background: #dc3545;">
|
||||
🗑️ Xóa Cache
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="loadCacheInfo()" style="font-size: 12px; padding: 5px 10px; background: #17a2b8; margin-left: 5px;">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; text-align: center;">
|
||||
<button type="submit" class="btn btn-primary" id="startBtn">
|
||||
🚀 Bắt Đầu Training
|
||||
@@ -526,15 +599,16 @@
|
||||
<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 style="margin-top: 15px; text-align: center;">
|
||||
<button onclick="downloadPredictionResult()" class="btn btn-secondary" style="background: #28a745;">
|
||||
📥 Tải Kết Quả
|
||||
</button>
|
||||
<button onclick="viewPredictionResult()" class="btn btn-secondary" style="background: #17a2b8; margin-left: 10px;">
|
||||
👁️ Xem Chi Tiết
|
||||
</button>
|
||||
<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>
|
||||
@@ -631,10 +705,12 @@
|
||||
max_scenes: parseInt(document.getElementById('maxScenes').value),
|
||||
cloud_cover: parseInt(document.getElementById('cloudCover').value),
|
||||
resolution: parseInt(document.getElementById('resolution').value),
|
||||
model_type: document.getElementById('modelType').value,
|
||||
n_estimators: parseInt(document.getElementById('nEstimators').value),
|
||||
max_depth: parseInt(document.getElementById('maxDepth').value),
|
||||
learning_rate: parseFloat(document.getElementById('learningRate').value),
|
||||
use_gpu: document.getElementById('useGpu').value === 'true'
|
||||
use_gpu: document.getElementById('useGpu').value === 'true',
|
||||
use_cache: document.getElementById('useCache').checked
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -994,10 +1070,209 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Model type change handler
|
||||
function updateModelTypeUI() {
|
||||
const modelType = document.getElementById('modelType').value;
|
||||
const desc = document.getElementById('modelTypeDesc');
|
||||
const nEstimatorsGroup = document.getElementById('nEstimatorsGroup');
|
||||
const learningRateGroup = document.getElementById('learningRateGroup');
|
||||
const useGpuGroup = document.getElementById('useGpuGroup');
|
||||
|
||||
const descriptions = {
|
||||
'xgboost': '✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh',
|
||||
'random_forest': '✓ Random Forest: Ổn định, không overfitting, phù hợp mọi kích thước dữ liệu',
|
||||
'decision_tree': '✓ Decision Tree: Đơn giản nhất, nhanh nhất, dễ hiểu, phù hợp để test nhanh',
|
||||
'svm': '✓ SVM: Chính xác cao với dữ liệu nhỏ, chậm với dữ liệu lớn',
|
||||
'cnn': '✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch'
|
||||
};
|
||||
|
||||
desc.textContent = descriptions[modelType];
|
||||
|
||||
// Show/hide parameters based on model type
|
||||
if (modelType === 'xgboost') {
|
||||
nEstimatorsGroup.style.display = '';
|
||||
learningRateGroup.style.display = '';
|
||||
useGpuGroup.style.display = '';
|
||||
} else if (modelType === 'random_forest') {
|
||||
nEstimatorsGroup.style.display = '';
|
||||
learningRateGroup.style.display = 'none';
|
||||
useGpuGroup.style.display = 'none';
|
||||
} else if (modelType === 'decision_tree') {
|
||||
nEstimatorsGroup.style.display = 'none';
|
||||
learningRateGroup.style.display = 'none';
|
||||
useGpuGroup.style.display = 'none';
|
||||
} else if (modelType === 'svm') {
|
||||
nEstimatorsGroup.style.display = 'none';
|
||||
learningRateGroup.style.display = 'none';
|
||||
useGpuGroup.style.display = 'none';
|
||||
} else if (modelType === 'cnn') {
|
||||
// CNN uses n_estimators as epochs and supports GPU
|
||||
nEstimatorsGroup.style.display = '';
|
||||
document.querySelector('#nEstimatorsGroup label').textContent = 'Epochs (số lần training):';
|
||||
document.getElementById('nEstimators').value = 50;
|
||||
learningRateGroup.style.display = 'none';
|
||||
useGpuGroup.style.display = ''; // Show GPU option for CNN
|
||||
}
|
||||
|
||||
// Reset n_estimators label for non-CNN
|
||||
if (modelType !== 'cnn' && modelType !== 'decision_tree' && modelType !== 'svm') {
|
||||
document.querySelector('#nEstimatorsGroup label').textContent = 'N Estimators:';
|
||||
}
|
||||
}
|
||||
|
||||
// ============== CACHE MANAGEMENT ==============
|
||||
|
||||
let cacheFiles = []; // Store cache files data
|
||||
|
||||
// Load cache info and populate preset dropdown
|
||||
async function loadCacheInfo() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/cache/info`);
|
||||
const data = await response.json();
|
||||
|
||||
// Update cache info display
|
||||
const cacheInfoSpan = document.getElementById('cacheInfo');
|
||||
if (data.exists && data.count > 0) {
|
||||
cacheInfoSpan.innerHTML = `✅ Có <strong>${data.count}</strong> file cache (${data.total_size_mb} MB)`;
|
||||
cacheInfoSpan.style.color = '#28a745';
|
||||
} else {
|
||||
cacheInfoSpan.innerHTML = '❌ Chưa có cache nào';
|
||||
cacheInfoSpan.style.color = '#6c757d';
|
||||
}
|
||||
|
||||
// Populate cache preset dropdown
|
||||
const cachePreset = document.getElementById('cachePreset');
|
||||
cachePreset.innerHTML = '<option value="">-- Không dùng cache preset --</option>';
|
||||
|
||||
if (data.exists && data.files && data.files.length > 0) {
|
||||
cacheFiles = data.files;
|
||||
data.files.forEach((file, index) => {
|
||||
const meta = file.metadata;
|
||||
const option = document.createElement('option');
|
||||
option.value = index;
|
||||
|
||||
// Format display text with metadata
|
||||
let displayText = `Cache #${index + 1}`;
|
||||
if (meta && meta.n_samples) {
|
||||
displayText += ` (${meta.n_samples} mẫu`;
|
||||
if (meta.start_date && meta.end_date) {
|
||||
displayText += `, ${meta.start_date} → ${meta.end_date}`;
|
||||
}
|
||||
if (meta.resolution) {
|
||||
displayText += `, ${meta.resolution}m`;
|
||||
}
|
||||
displayText += ')';
|
||||
} else {
|
||||
displayText += ` (${file.size_mb} MB, ${new Date(file.modified).toLocaleString('vi-VN')})`;
|
||||
}
|
||||
|
||||
option.textContent = displayText;
|
||||
cachePreset.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading cache info:', error);
|
||||
document.getElementById('cacheInfo').innerHTML = '⚠️ Lỗi kiểm tra cache';
|
||||
}
|
||||
}
|
||||
|
||||
// Apply cache preset to form inputs
|
||||
function applyCachePreset() {
|
||||
const selectIndex = document.getElementById('cachePreset').value;
|
||||
|
||||
if (selectIndex === '') {
|
||||
// Clear preset - no auto-fill
|
||||
return;
|
||||
}
|
||||
|
||||
const fileData = cacheFiles[parseInt(selectIndex)];
|
||||
if (!fileData || !fileData.metadata) {
|
||||
alert('Không có metadata cho cache này');
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = fileData.metadata;
|
||||
|
||||
// Fill bbox inputs
|
||||
if (meta.min_lon !== undefined) {
|
||||
document.getElementById('minLon').value = meta.min_lon;
|
||||
document.getElementById('maxLon').value = meta.max_lon;
|
||||
document.getElementById('minLat').value = meta.min_lat;
|
||||
document.getElementById('maxLat').value = meta.max_lat;
|
||||
|
||||
// Update map rectangle
|
||||
if (rectangle) {
|
||||
drawnItems.removeLayer(rectangle);
|
||||
}
|
||||
const bounds = L.latLngBounds(
|
||||
[meta.min_lat, meta.min_lon],
|
||||
[meta.max_lat, meta.max_lon]
|
||||
);
|
||||
rectangle = L.rectangle(bounds, {color: '#3388ff', weight: 3, fillOpacity: 0.2});
|
||||
drawnItems.addLayer(rectangle);
|
||||
map.fitBounds(bounds);
|
||||
}
|
||||
|
||||
// Fill time range inputs
|
||||
if (meta.start_date) {
|
||||
document.getElementById('startDate').value = meta.start_date;
|
||||
}
|
||||
if (meta.end_date) {
|
||||
document.getElementById('endDate').value = meta.end_date;
|
||||
}
|
||||
|
||||
// Fill resolution if available
|
||||
if (meta.resolution) {
|
||||
document.getElementById('resolution').value = meta.resolution;
|
||||
}
|
||||
|
||||
// Note to user
|
||||
const presetNote = `📌 Đã áp dụng cache preset: ${meta.n_samples || '?'} mẫu, ${meta.start_date || '?'} → ${meta.end_date || '?'}`;
|
||||
console.log(presetNote);
|
||||
|
||||
// Show notification
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = 'position:fixed;top:20px;right:20px;background:#28a745;color:white;padding:15px 20px;border-radius:8px;box-shadow:0 4px 6px rgba(0,0,0,0.1);z-index:10000;animation:slideIn 0.3s ease-out;';
|
||||
notification.innerHTML = `<strong>✅ Cache Preset Applied</strong><br>${presetNote}`;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease-out';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Clear cache
|
||||
async function clearCache() {
|
||||
if (!confirm('Bạn có chắc muốn xóa toàn bộ cache?\n\nCache giúp test nhanh hơn bằng cách lưu lại dữ liệu đã download.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/cache/clear`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
alert(result.message);
|
||||
loadCacheInfo(); // Refresh info
|
||||
} catch (error) {
|
||||
alert('Lỗi xóa cache: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize map when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initMap();
|
||||
initPredictionMap();
|
||||
loadPredictionsList(); // Load predictions list on page load
|
||||
loadCacheInfo(); // Load cache info
|
||||
|
||||
// Add model type change listener
|
||||
document.getElementById('modelType').addEventListener('change', updateModelTypeUI);
|
||||
updateModelTypeUI(); // Initial update
|
||||
|
||||
// Add cache preset change listener
|
||||
document.getElementById('cachePreset').addEventListener('change', applyCachePreset);
|
||||
});
|
||||
|
||||
// ============== PREDICTION FUNCTIONALITY ==============
|
||||
@@ -1207,6 +1482,10 @@
|
||||
// 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>
|
||||
@@ -1220,19 +1499,76 @@
|
||||
</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;
|
||||
alert('File kết quả: ' + result.output_file + '\n\nĐể tải file, vui lòng truy cập thư mục predictions/ trên server.');
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user