sửa các lỗi tại màn hình predict
This commit is contained in:
+222
-98
@@ -218,6 +218,16 @@ async def ndvi_page():
|
||||
raise HTTPException(status_code=404, detail="NDVI interface không tồn tại")
|
||||
|
||||
|
||||
@app.get("/reports", response_class=HTMLResponse)
|
||||
async def reports_page():
|
||||
"""Serve reports management interface"""
|
||||
html_file = Path(__file__).parent / "reports_interface.html"
|
||||
if html_file.exists():
|
||||
return FileResponse(html_file)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Reports interface 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"""
|
||||
@@ -324,7 +334,7 @@ async def clear_cache():
|
||||
|
||||
@app.get("/api/cache/info")
|
||||
async def get_cache_info():
|
||||
"""Lấy thông tin về cache với metadata đầy đủ"""
|
||||
"""Lấy thông tin về cache với metadata đầy đủ, tự động xóa cache cũ có lazy data"""
|
||||
cache_dir = Path("dataset_cache")
|
||||
|
||||
if not cache_dir.exists():
|
||||
@@ -332,16 +342,37 @@ async def get_cache_info():
|
||||
|
||||
cache_files = []
|
||||
total_size = 0
|
||||
deleted_count = 0
|
||||
|
||||
for cache_file in cache_dir.glob("*.joblib"):
|
||||
size = cache_file.stat().st_size
|
||||
total_size += size
|
||||
# Skip if file doesn't exist (race condition)
|
||||
if not cache_file.exists():
|
||||
continue
|
||||
|
||||
# Try to load metadata from cache
|
||||
size = cache_file.stat().st_size
|
||||
|
||||
# Try to load metadata from cache and check if it's valid
|
||||
metadata = {}
|
||||
is_valid = True
|
||||
try:
|
||||
cached_data = joblib.load(cache_file)
|
||||
if isinstance(cached_data, dict):
|
||||
|
||||
# Check if cache contains lazy data (will cause 403 errors)
|
||||
if isinstance(cached_data, dict) and "s2_data" in cached_data:
|
||||
s2_data_temp = cached_data["s2_data"]
|
||||
is_lazy = False
|
||||
try:
|
||||
is_lazy = any(hasattr(s2_data_temp[var].data, 'chunks') for var in s2_data_temp.data_vars)
|
||||
except:
|
||||
pass
|
||||
|
||||
if is_lazy:
|
||||
print(f"[CLEANUP] Deleting cache with lazy data: {cache_file.name}")
|
||||
cache_file.unlink()
|
||||
deleted_count += 1
|
||||
is_valid = False
|
||||
|
||||
if is_valid and isinstance(cached_data, dict):
|
||||
metadata = {
|
||||
"bbox": cached_data.get("bbox", []),
|
||||
"time_range": cached_data.get("time_range", ""),
|
||||
@@ -362,23 +393,36 @@ async def get_cache_info():
|
||||
metadata["max_lon"] = metadata["bbox"][2]
|
||||
metadata["max_lat"] = metadata["bbox"][3]
|
||||
except Exception as e:
|
||||
print(f"Error loading cache metadata: {e}")
|
||||
print(f"[CLEANUP] Error loading cache {cache_file.name}: {e}. Deleting...")
|
||||
try:
|
||||
cache_file.unlink()
|
||||
deleted_count += 1
|
||||
is_valid = False
|
||||
except:
|
||||
pass
|
||||
|
||||
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
|
||||
})
|
||||
# Only add valid cache files to the list
|
||||
if is_valid:
|
||||
total_size += size
|
||||
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)
|
||||
|
||||
if deleted_count > 0:
|
||||
print(f"[CLEANUP] Deleted {deleted_count} invalid cache files")
|
||||
|
||||
return {
|
||||
"exists": True,
|
||||
"files": cache_files,
|
||||
"count": len(cache_files),
|
||||
"total_size_mb": round(total_size / 1024 / 1024, 2)
|
||||
"total_size_mb": round(total_size / 1024 / 1024, 2),
|
||||
"deleted_invalid": deleted_count
|
||||
}
|
||||
|
||||
|
||||
@@ -717,15 +761,44 @@ async def run_prediction(config: PredictionConfig):
|
||||
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
|
||||
time_range = f"{config.start_date}/{config.end_date}"
|
||||
|
||||
# Try to load from cache first
|
||||
s2_data = None
|
||||
use_cache = False
|
||||
|
||||
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:
|
||||
try:
|
||||
cached = joblib.load(cache_file)
|
||||
s2_data_temp = cached["s2_data"]
|
||||
|
||||
# Verify that cached data is not lazy (to avoid 403 errors from expired URLs)
|
||||
# If s2_data has chunks attribute, it's a dask array (lazy)
|
||||
is_lazy = False
|
||||
try:
|
||||
is_lazy = any(hasattr(s2_data_temp[var].data, 'chunks') for var in s2_data_temp.data_vars)
|
||||
except:
|
||||
pass
|
||||
|
||||
if is_lazy:
|
||||
print(f"[WARNING] Cache contains lazy data with potentially expired URLs. Deleting cache...")
|
||||
cache_file.unlink()
|
||||
raise ValueError("Cache invalid - contains lazy data")
|
||||
|
||||
# Cache is valid, use it
|
||||
s2_data = s2_data_temp
|
||||
s2_items = cached.get("s2_items", [])
|
||||
vh_monthly = cached.get("vh_monthly")
|
||||
vv_monthly = cached.get("vv_monthly")
|
||||
use_radar = cached.get("use_radar", False)
|
||||
use_cache = True
|
||||
print(f"[INFO] Loaded valid cache from {cache_file.name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Failed to load cache: {e}. Fetching fresh data...")
|
||||
s2_data = None
|
||||
|
||||
# If cache not available or invalid, fetch from Microsoft
|
||||
if s2_data is None:
|
||||
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
||||
import pystac_client
|
||||
import planetary_computer
|
||||
@@ -747,13 +820,17 @@ async def run_prediction(config: PredictionConfig):
|
||||
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_data_lazy = load(
|
||||
s2_items,
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="solar_day",
|
||||
resolution=config.resolution
|
||||
)
|
||||
# Compute s2_data to load into memory (avoid lazy loading from expired URLs)
|
||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2 vào bộ nhớ..."
|
||||
s2_data = s2_data_lazy.compute()
|
||||
|
||||
# ============ 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(
|
||||
@@ -828,59 +905,65 @@ async def run_prediction(config: PredictionConfig):
|
||||
# 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)..."
|
||||
|
||||
# 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,
|
||||
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..."
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# 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
|
||||
try:
|
||||
# Initialize catalog if not already done
|
||||
if not cache_file.exists():
|
||||
pass
|
||||
else:
|
||||
prediction_status["progress"] = "Không tìm thấy bands VH/VV, tiếp tục với NDVI..."
|
||||
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,
|
||||
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..."
|
||||
try:
|
||||
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')
|
||||
prediction_status["progress"] = "Đang tính trung bình VH/VV theo tháng..."
|
||||
try:
|
||||
vh_monthly = vh.resample(time="1ME").mean().compute()
|
||||
vv_monthly = vv.resample(time="1ME").mean().compute()
|
||||
use_radar = True
|
||||
except Exception as radar_exc:
|
||||
print(f"[RADAR WARNING] Không thể tính radar monthly: {radar_exc}")
|
||||
vh_monthly = None
|
||||
vv_monthly = None
|
||||
use_radar = False
|
||||
else:
|
||||
prediction_status["progress"] = "Không tìm thấy bands VH/VV, tiếp tục với NDVI..."
|
||||
use_radar = False
|
||||
except Exception as radar_exc:
|
||||
print(f"[RADAR WARNING] Không thể tải dữ liệu Sentinel-1: {radar_exc}")
|
||||
vh_monthly = None
|
||||
vv_monthly = None
|
||||
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
|
||||
else:
|
||||
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, tiếp tục với NDVI..."
|
||||
except Exception as radar_exc:
|
||||
print(f"[RADAR WARNING] Không thể truy cập Sentinel-1: {radar_exc}")
|
||||
prediction_status["progress"] = "Không thể truy cập Sentinel-1, tiếp tục với NDVI..."
|
||||
vh_monthly = None
|
||||
vv_monthly = None
|
||||
use_radar = False
|
||||
|
||||
# ============ BƯỚC 5: CHUẨN BỊ FEATURES CHO DỰ ĐOÁN ============
|
||||
@@ -1776,46 +1859,87 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
|
||||
import odc.stac
|
||||
import rasterio
|
||||
from rasterio.transform import from_bounds
|
||||
import hashlib, os
|
||||
|
||||
# Load model
|
||||
model_path = Path(f"model_train/{config.model_filename}")
|
||||
if not model_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Model {config.model_filename} không tồn tại")
|
||||
|
||||
model = joblib.load(model_path)
|
||||
model_data = joblib.load(model_path)
|
||||
|
||||
# Extract model from dict (models are saved as {'model': xgb_model, 'label_encoder': encoder})
|
||||
if isinstance(model_data, dict):
|
||||
model = model_data.get('model')
|
||||
label_encoder = model_data.get('label_encoder')
|
||||
else:
|
||||
model = model_data
|
||||
label_encoder = None
|
||||
|
||||
print(f"[PREDICT+NDVI] Loaded model: {config.model_filename}")
|
||||
|
||||
# Connect to Microsoft Planetary Computer
|
||||
catalog = Client.open(
|
||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||
modifier=planetary_computer.sign_inplace
|
||||
)
|
||||
# Check cache first
|
||||
cache_dir = Path("dataset_cache")
|
||||
cache_dir.mkdir(exist_ok=True)
|
||||
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"
|
||||
|
||||
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
|
||||
time_range = f"{config.start_date}/{config.end_date}"
|
||||
|
||||
# Search for Sentinel-2 data
|
||||
search = catalog.search(
|
||||
collections=["sentinel-2-l2a"],
|
||||
bbox=bbox,
|
||||
datetime=time_range,
|
||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||
)
|
||||
# Load from cache or fetch from Microsoft
|
||||
if cache_file.exists():
|
||||
print(f"[PREDICT+NDVI] Loading from cache: {cache_file.name}")
|
||||
cached = joblib.load(cache_file)
|
||||
# Extract s2_data from cache (already computed in cache)
|
||||
s2_data = cached["s2_data"]
|
||||
# Check if needed bands are available in cache
|
||||
available_bands = list(s2_data.data_vars.keys())
|
||||
needed_bands = ["B02", "B03", "B04", "B08"]
|
||||
if all(band in available_bands for band in needed_bands):
|
||||
# Use cached data directly (no need to compute again)
|
||||
data = s2_data[needed_bands]
|
||||
print(f"[PREDICT+NDVI] Using cached bands: {needed_bands}")
|
||||
# Set items to match the number of time slices in the cached data
|
||||
items = [None] * s2_data.sizes.get("time", 1)
|
||||
else:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Cache thiếu bands cần thiết. Có: {available_bands}, Cần: {needed_bands}")
|
||||
else:
|
||||
print(f"[PREDICT+NDVI] No cache found, fetching from Microsoft Planetary Computer")
|
||||
# Connect to Microsoft Planetary Computer
|
||||
catalog = Client.open(
|
||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||
modifier=planetary_computer.sign_inplace
|
||||
)
|
||||
|
||||
items = list(search.items())[:config.max_scenes]
|
||||
print(f"[PREDICT+NDVI] Found {len(items)} Sentinel-2 scenes")
|
||||
time_range = f"{config.start_date}/{config.end_date}"
|
||||
|
||||
if len(items) == 0:
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy dữ liệu vệ tinh")
|
||||
# Search for Sentinel-2 data
|
||||
search = catalog.search(
|
||||
collections=["sentinel-2-l2a"],
|
||||
bbox=bbox,
|
||||
datetime=time_range,
|
||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||
)
|
||||
|
||||
# Load all bands needed for features
|
||||
data = odc.stac.load(
|
||||
items,
|
||||
bbox=bbox,
|
||||
bands=["B02", "B03", "B04", "B08"], # Blue, Green, Red, NIR
|
||||
resolution=config.resolution,
|
||||
chunks={"x": 2048, "y": 2048}
|
||||
).compute()
|
||||
items = list(search.items())[:config.max_scenes]
|
||||
print(f"[PREDICT+NDVI] Found {len(items)} Sentinel-2 scenes")
|
||||
|
||||
if len(items) == 0:
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy dữ liệu vệ tinh")
|
||||
|
||||
# Sign items to refresh SAS tokens (keep as pystac.Item, not dict)
|
||||
signed_items = [planetary_computer.sign(item) for item in items]
|
||||
|
||||
# Load all bands needed for features
|
||||
data = odc.stac.load(
|
||||
signed_items,
|
||||
bbox=bbox,
|
||||
bands=["B02", "B03", "B04", "B08"], # Blue, Green, Red, NIR
|
||||
resolution=config.resolution,
|
||||
chunks={"x": 2048, "y": 2048}
|
||||
).compute()
|
||||
|
||||
print(f"[PREDICT+NDVI] Loaded data shape: {data.dims}")
|
||||
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</a>
|
||||
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing (Active)</a>
|
||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
|
||||
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
@@ -433,6 +433,7 @@
|
||||
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</a>
|
||||
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing</a>
|
||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
|
||||
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Home -->
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</a>
|
||||
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing</a>
|
||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis (Active)</a>
|
||||
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
@@ -298,6 +298,7 @@
|
||||
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction (Active)</a>
|
||||
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing</a>
|
||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
|
||||
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
@@ -957,6 +958,10 @@
|
||||
const data = await response.json();
|
||||
const select = document.getElementById('cacheSelect');
|
||||
select.innerHTML = '<option value="">-- Không dùng cache --</option>';
|
||||
|
||||
// Store cache data globally for later use
|
||||
window.cacheFiles = data.files || [];
|
||||
|
||||
if (data.files && data.files.length > 0) {
|
||||
data.files.forEach((file, idx) => {
|
||||
if (file.filename.startsWith('prediction_input_')) {
|
||||
@@ -976,6 +981,80 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Apply cache preset - auto fill bbox and other params
|
||||
function applyCachePreset() {
|
||||
const selectValue = document.getElementById('cacheSelect').value;
|
||||
if (!selectValue || !window.cacheFiles) return;
|
||||
|
||||
// Find selected cache file
|
||||
const cacheFile = window.cacheFiles.find(f => f.filename === selectValue);
|
||||
if (!cacheFile || !cacheFile.metadata) {
|
||||
console.warn('No metadata found for selected cache');
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = cacheFile.metadata;
|
||||
|
||||
// Auto-fill bbox from cache
|
||||
if (meta.bbox && meta.bbox.length === 4) {
|
||||
const [min_lon, min_lat, max_lon, max_lat] = meta.bbox;
|
||||
|
||||
// Set selectedBbox directly without drawing on map
|
||||
selectedBbox = {
|
||||
min_lon: min_lon,
|
||||
min_lat: min_lat,
|
||||
max_lon: max_lon,
|
||||
max_lat: max_lat
|
||||
};
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('prediction_bbox', JSON.stringify(selectedBbox));
|
||||
|
||||
// Draw rectangle on map to visualize
|
||||
if (currentRectangle) {
|
||||
drawnItems.removeLayer(currentRectangle);
|
||||
}
|
||||
|
||||
const bounds = L.latLngBounds(
|
||||
[min_lat, min_lon],
|
||||
[max_lat, max_lon]
|
||||
);
|
||||
|
||||
currentRectangle = L.rectangle(bounds, {
|
||||
color: '#4facfe',
|
||||
weight: 3,
|
||||
fillOpacity: 0.2
|
||||
});
|
||||
|
||||
drawnItems.addLayer(currentRectangle);
|
||||
map.fitBounds(bounds);
|
||||
|
||||
console.log(`✅ Auto-applied bbox from cache: [${min_lon}, ${min_lat}, ${max_lon}, ${max_lat}]`);
|
||||
}
|
||||
|
||||
// Auto-fill time range if available
|
||||
if (meta.start_date) {
|
||||
document.getElementById('predStartDate').value = meta.start_date;
|
||||
}
|
||||
if (meta.end_date) {
|
||||
document.getElementById('predEndDate').value = meta.end_date;
|
||||
}
|
||||
|
||||
// Auto-fill other params
|
||||
if (meta.resolution) {
|
||||
document.getElementById('predResolution').value = meta.resolution;
|
||||
}
|
||||
if (meta.max_scenes) {
|
||||
document.getElementById('predMaxScenes').value = meta.max_scenes;
|
||||
}
|
||||
if (meta.cloud_cover) {
|
||||
document.getElementById('predCloudCover').value = meta.cloud_cover;
|
||||
}
|
||||
|
||||
// Show notification
|
||||
alert(`✅ Đã áp dụng cache preset!\n\nBBox: [${meta.bbox?.join(', ') || 'N/A'}]\nTime: ${meta.start_date || '?'} → ${meta.end_date || '?'}\n\nBạn có thể predict ngay mà không cần vẽ bbox!`);
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
window.onload = function() {
|
||||
initMap();
|
||||
@@ -984,6 +1063,8 @@
|
||||
loadCacheList();
|
||||
// Add event listener for model selection
|
||||
document.getElementById('modelSelect').addEventListener('change', updateModelInfo);
|
||||
// Add event listener for cache selection
|
||||
document.getElementById('cacheSelect').addEventListener('change', applyCachePreset);
|
||||
};
|
||||
|
||||
// Cleanup on page unload
|
||||
|
||||
@@ -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 - 20251222_115713</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 - 22/12/2025 11:57:13</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">165</div>
|
||||
<div class="label">Tổng số Pixels</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value">11x15</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_xgboost_20251221_172351.joblib</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">📍 Khu vực (bbox):</span>
|
||||
<span>[105.47426033018384, 9.250032954766686, 105.47683525083814, 9.251917848893436]</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_20251222_115712.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: 22/12/2025 11:57:13</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 - 20251222_153138</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 - 22/12/2025 15:31:38</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">165</div>
|
||||
<div class="label">Tổng số Pixels</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value">11x15</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_xgboost_20251221_172351.joblib</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">📍 Khu vực (bbox):</span>
|
||||
<span>[105.47426033018384, 9.250032954766686, 105.47683525083814, 9.251917848893436]</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_20251222_153137.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: 22/12/2025 15:31:38</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 - 20251222_154215</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 - 22/12/2025 15:42: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">1,053</div>
|
||||
<div class="label">Tổng số Pixels</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value">27x39</div>
|
||||
<div class="label">Kích thước (px)</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value">0.3</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.54108810418259, 9.340180964398723, 105.54791164391646, 9.344839034909683]</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_20251222_154215.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: 22/12/2025 15:42: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 - 20251222_154237</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 - 22/12/2025 15:42:37</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">1,053</div>
|
||||
<div class="label">Tổng số Pixels</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value">27x39</div>
|
||||
<div class="label">Kích thước (px)</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value">0.3</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.54108810418259, 9.340180964398723, 105.54791164391646, 9.344839034909683]</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_20251222_154237.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: 22/12/2025 15:42:37</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,436 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reports Management</title>
|
||||
|
||||
<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;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
color: #667eea;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reports-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.report-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.report-card h3 {
|
||||
color: #667eea;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.report-card .meta {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.report-card .badge {
|
||||
display: inline-block;
|
||||
padding: 5px 12px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.badge-training {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-prediction {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-batch {
|
||||
background: #feca57;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.report-card .actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.report-card .btn {
|
||||
padding: 8px 15px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border-left: 4px solid #667eea;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
color: #667eea;
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 10px 20px;
|
||||
background: white;
|
||||
border: 2px solid #667eea;
|
||||
color: #667eea;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.filter-btn:hover,
|
||||
.filter-btn.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📝 Reports Management</h1>
|
||||
<p>Quản lý báo cáo training và prediction</p>
|
||||
</div>
|
||||
|
||||
<div style="background: white; padding: 15px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; border-bottom: 2px solid #e0e0e0;">
|
||||
<a href="/" style="padding: 10px 20px; background: #667eea; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🏠 Trang Chủ</a>
|
||||
<a href="/training" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🎓 Training</a>
|
||||
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</a>
|
||||
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing</a>
|
||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
|
||||
<a href="/reports" style="padding: 10px 20px; background: #f093fb; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports (Active)</a>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Statistics -->
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<div class="stat-card">
|
||||
<h3>📊 Total Reports</h3>
|
||||
<div class="value" id="statTotal">0</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left-color: #667eea;">
|
||||
<h3>🎓 Training Reports</h3>
|
||||
<div class="value" id="statTraining" style="color: #667eea;">0</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left-color: #ff6b6b;">
|
||||
<h3>🗺️ Prediction Reports</h3>
|
||||
<div class="value" id="statPrediction" style="color: #ff6b6b;">0</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left-color: #feca57;">
|
||||
<h3>🚀 Batch Reports</h3>
|
||||
<div class="value" id="statBatch" style="color: #feca57;">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filter-section">
|
||||
<button class="filter-btn active" onclick="filterReports('all')">Tất cả</button>
|
||||
<button class="filter-btn" onclick="filterReports('training')">Training</button>
|
||||
<button class="filter-btn" onclick="filterReports('prediction')">Prediction</button>
|
||||
<button class="filter-btn" onclick="filterReports('batch')">Batch Jobs</button>
|
||||
<button class="btn btn-secondary" onclick="loadReports()" style="margin-left: auto;">🔄 Refresh</button>
|
||||
</div>
|
||||
|
||||
<!-- Reports Grid -->
|
||||
<div class="section">
|
||||
<div id="reportsGrid" class="reports-grid">
|
||||
<div class="empty-state">
|
||||
<p>⏳ Đang tải...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:8000/api';
|
||||
let allReports = [];
|
||||
let currentFilter = 'all';
|
||||
|
||||
// Load reports on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadReports();
|
||||
});
|
||||
|
||||
async function loadReports() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/reports/list`);
|
||||
const data = await response.json();
|
||||
|
||||
allReports = data.reports;
|
||||
updateStats(data.reports);
|
||||
displayReports(filterReportsByType(data.reports, currentFilter));
|
||||
} catch (error) {
|
||||
console.error('Error loading reports:', error);
|
||||
document.getElementById('reportsGrid').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<p style="color: red;">❌ Lỗi khi tải reports: ${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats(reports) {
|
||||
const total = reports.length;
|
||||
const training = reports.filter(r => r.type === 'training').length;
|
||||
const prediction = reports.filter(r => r.type === 'prediction').length;
|
||||
const batch = reports.filter(r => r.is_batch_job).length;
|
||||
|
||||
document.getElementById('statTotal').textContent = total;
|
||||
document.getElementById('statTraining').textContent = training;
|
||||
document.getElementById('statPrediction').textContent = prediction;
|
||||
document.getElementById('statBatch').textContent = batch;
|
||||
}
|
||||
|
||||
function filterReports(type) {
|
||||
currentFilter = type;
|
||||
|
||||
// Update active button
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Filter and display
|
||||
const filtered = filterReportsByType(allReports, type);
|
||||
displayReports(filtered);
|
||||
}
|
||||
|
||||
function filterReportsByType(reports, type) {
|
||||
if (type === 'all') return reports;
|
||||
if (type === 'batch') return reports.filter(r => r.is_batch_job);
|
||||
return reports.filter(r => r.type === type);
|
||||
}
|
||||
|
||||
function displayReports(reports) {
|
||||
const grid = document.getElementById('reportsGrid');
|
||||
|
||||
if (reports.length === 0) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<p>📝 Không có báo cáo nào</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = reports.map(report => {
|
||||
const badgeClass = report.type === 'training' ? 'badge-training' : 'badge-prediction';
|
||||
const badgeText = report.type === 'training' ? '🎓 Training' : '🗺️ Prediction';
|
||||
const batchBadge = report.is_batch_job ? '<span class="badge badge-batch">🚀 Batch Job</span>' : '';
|
||||
|
||||
const createdDate = new Date(report.created).toLocaleString('vi-VN');
|
||||
|
||||
let metaInfo = `
|
||||
<p>📅 ${createdDate}</p>
|
||||
<p>💾 ${report.size_kb} KB</p>
|
||||
`;
|
||||
|
||||
if (report.batch_metadata) {
|
||||
metaInfo += `
|
||||
<p style="margin-top: 5px; font-weight: 600;">
|
||||
📦 ${report.batch_metadata.batch_name || 'Batch Job'}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="report-card">
|
||||
<span class="badge ${badgeClass}">${badgeText}</span>
|
||||
${batchBadge}
|
||||
<h3>📄 ${report.filename}</h3>
|
||||
<div class="meta">
|
||||
${metaInfo}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" onclick="viewReport('${report.filename}')">
|
||||
👁️ Xem
|
||||
</button>
|
||||
<button class="btn btn-success" onclick="downloadReport('${report.filename}')">
|
||||
💾 Tải
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="deleteReport('${report.filename}')">
|
||||
🗑️ Xóa
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function viewReport(filename) {
|
||||
window.open(`${API_BASE}/reports/view/${filename}`, '_blank');
|
||||
}
|
||||
|
||||
function downloadReport(filename) {
|
||||
window.location.href = `${API_BASE}/reports/download/${filename}`;
|
||||
}
|
||||
|
||||
async function deleteReport(filename) {
|
||||
if (!confirm(`Bạn có chắc muốn xóa báo cáo: ${filename}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/reports/delete/${filename}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('✅ Đã xóa báo cáo thành công!');
|
||||
loadReports();
|
||||
} else {
|
||||
alert('❌ Không thể xóa báo cáo!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting report:', error);
|
||||
alert('❌ Lỗi khi xóa báo cáo: ' + error.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +1,3 @@
|
||||
uvicorn api_server:app --reload --host 0.0.0.0 --port 8000
|
||||
|
||||
#uvicorn api_server:app --reload --host 0.0.0.0 --port 8000
|
||||
pkill -f "uvicorn api_server:app" && sleep 1 && nohup uvicorn api_server:app --host 0.0.0.0 --port 8000 > server.log 2>&1 &
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</a>
|
||||
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing</a>
|
||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
|
||||
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
Reference in New Issue
Block a user