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")
|
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")
|
@app.get("/api/config/presets")
|
||||||
async def get_presets():
|
async def get_presets():
|
||||||
"""Lấy các preset cấu hình sẵn"""
|
"""Lấy các preset cấu hình sẵn"""
|
||||||
@@ -324,7 +334,7 @@ async def clear_cache():
|
|||||||
|
|
||||||
@app.get("/api/cache/info")
|
@app.get("/api/cache/info")
|
||||||
async def get_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")
|
cache_dir = Path("dataset_cache")
|
||||||
|
|
||||||
if not cache_dir.exists():
|
if not cache_dir.exists():
|
||||||
@@ -332,16 +342,37 @@ async def get_cache_info():
|
|||||||
|
|
||||||
cache_files = []
|
cache_files = []
|
||||||
total_size = 0
|
total_size = 0
|
||||||
|
deleted_count = 0
|
||||||
|
|
||||||
for cache_file in cache_dir.glob("*.joblib"):
|
for cache_file in cache_dir.glob("*.joblib"):
|
||||||
size = cache_file.stat().st_size
|
# Skip if file doesn't exist (race condition)
|
||||||
total_size += size
|
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 = {}
|
metadata = {}
|
||||||
|
is_valid = True
|
||||||
try:
|
try:
|
||||||
cached_data = joblib.load(cache_file)
|
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 = {
|
metadata = {
|
||||||
"bbox": cached_data.get("bbox", []),
|
"bbox": cached_data.get("bbox", []),
|
||||||
"time_range": cached_data.get("time_range", ""),
|
"time_range": cached_data.get("time_range", ""),
|
||||||
@@ -362,23 +393,36 @@ async def get_cache_info():
|
|||||||
metadata["max_lon"] = metadata["bbox"][2]
|
metadata["max_lon"] = metadata["bbox"][2]
|
||||||
metadata["max_lat"] = metadata["bbox"][3]
|
metadata["max_lat"] = metadata["bbox"][3]
|
||||||
except Exception as e:
|
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({
|
# Only add valid cache files to the list
|
||||||
"filename": cache_file.name,
|
if is_valid:
|
||||||
"size_mb": round(size / 1024 / 1024, 2),
|
total_size += size
|
||||||
"modified": datetime.fromtimestamp(cache_file.stat().st_mtime).isoformat(),
|
cache_files.append({
|
||||||
"metadata": metadata
|
"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)
|
# Sort by modified time (newest first)
|
||||||
cache_files.sort(key=lambda x: x["modified"], reverse=True)
|
cache_files.sort(key=lambda x: x["modified"], reverse=True)
|
||||||
|
|
||||||
|
if deleted_count > 0:
|
||||||
|
print(f"[CLEANUP] Deleted {deleted_count} invalid cache files")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"exists": True,
|
"exists": True,
|
||||||
"files": cache_files,
|
"files": cache_files,
|
||||||
"count": len(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]
|
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
|
||||||
time_range = f"{config.start_date}/{config.end_date}"
|
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():
|
if cache_file.exists():
|
||||||
prediction_status["progress"] = "Đang load dữ liệu từ cache..."
|
prediction_status["progress"] = "Đang load dữ liệu từ cache..."
|
||||||
cached = joblib.load(cache_file)
|
try:
|
||||||
s2_data = cached["s2_data"]
|
cached = joblib.load(cache_file)
|
||||||
s2_items = cached["s2_items"]
|
s2_data_temp = cached["s2_data"]
|
||||||
vh_monthly = cached.get("vh_monthly")
|
|
||||||
vv_monthly = cached.get("vv_monthly")
|
# Verify that cached data is not lazy (to avoid 403 errors from expired URLs)
|
||||||
use_radar = cached.get("use_radar", False)
|
# If s2_data has chunks attribute, it's a dask array (lazy)
|
||||||
else:
|
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..."
|
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
||||||
import pystac_client
|
import pystac_client
|
||||||
import planetary_computer
|
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")
|
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
||||||
s2_items = s2_items[:config.max_scenes]
|
s2_items = s2_items[:config.max_scenes]
|
||||||
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
||||||
s2_data = load(
|
s2_data_lazy = load(
|
||||||
s2_items,
|
s2_items,
|
||||||
bbox=bbox,
|
bbox=bbox,
|
||||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||||
groupby="solar_day",
|
groupby="solar_day",
|
||||||
resolution=config.resolution
|
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)... ============
|
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (Radar)... ============
|
||||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
||||||
s1_search = catalog.search(
|
s1_search = catalog.search(
|
||||||
@@ -828,59 +905,65 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
# Only load radar if not already in cache
|
# Only load radar if not already in cache
|
||||||
if not cache_file.exists() or (cache_file.exists() and not use_radar):
|
if not cache_file.exists() or (cache_file.exists() and not use_radar):
|
||||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
||||||
|
try:
|
||||||
# Initialize catalog if not already done
|
# Initialize catalog if not already done
|
||||||
if not cache_file.exists():
|
if not cache_file.exists():
|
||||||
# catalog already initialized in the else block above
|
pass
|
||||||
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
|
|
||||||
else:
|
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
|
use_radar = False
|
||||||
else:
|
except Exception as radar_exc:
|
||||||
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, tiếp tục với NDVI..."
|
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
|
use_radar = False
|
||||||
|
|
||||||
# ============ BƯỚC 5: CHUẨN BỊ FEATURES CHO DỰ ĐOÁN ============
|
# ============ 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 odc.stac
|
||||||
import rasterio
|
import rasterio
|
||||||
from rasterio.transform import from_bounds
|
from rasterio.transform import from_bounds
|
||||||
|
import hashlib, os
|
||||||
|
|
||||||
# Load model
|
# Load model
|
||||||
model_path = Path(f"model_train/{config.model_filename}")
|
model_path = Path(f"model_train/{config.model_filename}")
|
||||||
if not model_path.exists():
|
if not model_path.exists():
|
||||||
raise HTTPException(status_code=404, detail=f"Model {config.model_filename} không tồn tại")
|
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}")
|
print(f"[PREDICT+NDVI] Loaded model: {config.model_filename}")
|
||||||
|
|
||||||
# Connect to Microsoft Planetary Computer
|
# Check cache first
|
||||||
catalog = Client.open(
|
cache_dir = Path("dataset_cache")
|
||||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
cache_dir.mkdir(exist_ok=True)
|
||||||
modifier=planetary_computer.sign_inplace
|
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]
|
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
|
# Load from cache or fetch from Microsoft
|
||||||
search = catalog.search(
|
if cache_file.exists():
|
||||||
collections=["sentinel-2-l2a"],
|
print(f"[PREDICT+NDVI] Loading from cache: {cache_file.name}")
|
||||||
bbox=bbox,
|
cached = joblib.load(cache_file)
|
||||||
datetime=time_range,
|
# Extract s2_data from cache (already computed in cache)
|
||||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
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]
|
time_range = f"{config.start_date}/{config.end_date}"
|
||||||
print(f"[PREDICT+NDVI] Found {len(items)} Sentinel-2 scenes")
|
|
||||||
|
|
||||||
if len(items) == 0:
|
# Search for Sentinel-2 data
|
||||||
raise HTTPException(status_code=404, detail="Không tìm thấy dữ liệu vệ tinh")
|
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
|
items = list(search.items())[:config.max_scenes]
|
||||||
data = odc.stac.load(
|
print(f"[PREDICT+NDVI] Found {len(items)} Sentinel-2 scenes")
|
||||||
items,
|
|
||||||
bbox=bbox,
|
if len(items) == 0:
|
||||||
bands=["B02", "B03", "B04", "B08"], # Blue, Green, Red, NIR
|
raise HTTPException(status_code=404, detail="Không tìm thấy dữ liệu vệ tinh")
|
||||||
resolution=config.resolution,
|
|
||||||
chunks={"x": 2048, "y": 2048}
|
# Sign items to refresh SAS tokens (keep as pystac.Item, not dict)
|
||||||
).compute()
|
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}")
|
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="/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="/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="/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>
|
||||||
|
|
||||||
<div class="content">
|
<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="/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="/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="/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>
|
||||||
|
|
||||||
<!-- Tab Content: Home -->
|
<!-- 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="/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="/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="/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>
|
||||||
|
|
||||||
<div class="content">
|
<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="/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="/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="/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>
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
@@ -957,6 +958,10 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const select = document.getElementById('cacheSelect');
|
const select = document.getElementById('cacheSelect');
|
||||||
select.innerHTML = '<option value="">-- Không dùng cache --</option>';
|
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) {
|
if (data.files && data.files.length > 0) {
|
||||||
data.files.forEach((file, idx) => {
|
data.files.forEach((file, idx) => {
|
||||||
if (file.filename.startsWith('prediction_input_')) {
|
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
|
// Initialize on page load
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
initMap();
|
initMap();
|
||||||
@@ -984,6 +1063,8 @@
|
|||||||
loadCacheList();
|
loadCacheList();
|
||||||
// Add event listener for model selection
|
// Add event listener for model selection
|
||||||
document.getElementById('modelSelect').addEventListener('change', updateModelInfo);
|
document.getElementById('modelSelect').addEventListener('change', updateModelInfo);
|
||||||
|
// Add event listener for cache selection
|
||||||
|
document.getElementById('cacheSelect').addEventListener('change', applyCachePreset);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cleanup on page unload
|
// 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="/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="/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="/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>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|||||||
Reference in New Issue
Block a user