hoàn thành chức năng phân lô trên ảnh predict
This commit is contained in:
+291
-1
@@ -224,6 +224,9 @@ class PredictionConfig(BaseModel):
|
|||||||
# Cloud removal strategy
|
# Cloud removal strategy
|
||||||
cloud_removal_method: str = "classic"
|
cloud_removal_method: str = "classic"
|
||||||
cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
|
cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
|
||||||
|
|
||||||
|
# Shapefile overlay for visualization
|
||||||
|
shapefile_overlay: Optional[str] = None # Path to shapefile for overlaying boundaries
|
||||||
|
|
||||||
|
|
||||||
class TrainingStatus(BaseModel):
|
class TrainingStatus(BaseModel):
|
||||||
@@ -286,6 +289,7 @@ class PredictionWithNDVIConfig(BaseModel):
|
|||||||
export_classification: bool = True # Export classification raster
|
export_classification: bool = True # Export classification raster
|
||||||
cloud_removal_method: str = "classic"
|
cloud_removal_method: str = "classic"
|
||||||
cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
|
cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
|
||||||
|
shapefile_overlay: Optional[str] = None # Path to shapefile for overlaying boundaries
|
||||||
|
|
||||||
|
|
||||||
class CloudRemovalTrainingConfig(BaseModel):
|
class CloudRemovalTrainingConfig(BaseModel):
|
||||||
@@ -1228,6 +1232,87 @@ async def list_training_files():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/overlay/shapefiles")
|
||||||
|
async def list_overlay_shapefiles():
|
||||||
|
"""Liệt kê các shapefile có sẵn cho overlay trên prediction"""
|
||||||
|
overlay_dirs = ["region", "ChauThanh", "ThuanHoa"]
|
||||||
|
shapefiles = []
|
||||||
|
|
||||||
|
for overlay_dir in overlay_dirs:
|
||||||
|
dir_path = Path(overlay_dir)
|
||||||
|
if not dir_path.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find all .shp files in this directory and subdirectories
|
||||||
|
for shp_file in dir_path.rglob("*.shp"):
|
||||||
|
try:
|
||||||
|
file_size = shp_file.stat().st_size
|
||||||
|
file_modified = datetime.fromtimestamp(shp_file.stat().st_mtime).isoformat()
|
||||||
|
|
||||||
|
# Try to read shapefile to get feature count and bbox
|
||||||
|
import geopandas as gpd
|
||||||
|
gdf = gpd.read_file(str(shp_file))
|
||||||
|
feature_count = len(gdf)
|
||||||
|
|
||||||
|
# Calculate bbox (always in EPSG:4326 for consistency)
|
||||||
|
bbox = None
|
||||||
|
if not gdf.empty and gdf.crs:
|
||||||
|
try:
|
||||||
|
# Reproject to EPSG:4326 if needed
|
||||||
|
if gdf.crs != "EPSG:4326":
|
||||||
|
gdf_4326 = gdf.to_crs("EPSG:4326")
|
||||||
|
else:
|
||||||
|
gdf_4326 = gdf
|
||||||
|
|
||||||
|
# Get total bounds [minx, miny, maxx, maxy]
|
||||||
|
bounds = gdf_4326.total_bounds
|
||||||
|
if len(bounds) == 4:
|
||||||
|
bbox = [
|
||||||
|
float(bounds[0]), # min_lon
|
||||||
|
float(bounds[1]), # min_lat
|
||||||
|
float(bounds[2]), # max_lon
|
||||||
|
float(bounds[3]) # max_lat
|
||||||
|
]
|
||||||
|
except Exception as bbox_error:
|
||||||
|
print(f"[WARNING] Cannot calculate bbox for {shp_file}: {bbox_error}")
|
||||||
|
|
||||||
|
# Get relative path from workspace root
|
||||||
|
relative_path = str(shp_file)
|
||||||
|
|
||||||
|
shapefiles.append({
|
||||||
|
"filename": shp_file.name,
|
||||||
|
"path": relative_path,
|
||||||
|
"directory": overlay_dir,
|
||||||
|
"size_bytes": file_size,
|
||||||
|
"size_mb": round(file_size / 1024 / 1024, 2),
|
||||||
|
"modified": file_modified,
|
||||||
|
"feature_count": feature_count,
|
||||||
|
"crs": str(gdf.crs) if gdf.crs else "Unknown",
|
||||||
|
"bbox": bbox # [min_lon, min_lat, max_lon, max_lat] in EPSG:4326
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
# If cannot read shapefile, just add basic info
|
||||||
|
file_size = shp_file.stat().st_size
|
||||||
|
file_modified = datetime.fromtimestamp(shp_file.stat().st_mtime).isoformat()
|
||||||
|
relative_path = str(shp_file)
|
||||||
|
|
||||||
|
shapefiles.append({
|
||||||
|
"filename": shp_file.name,
|
||||||
|
"path": relative_path,
|
||||||
|
"directory": overlay_dir,
|
||||||
|
"size_bytes": file_size,
|
||||||
|
"size_mb": round(file_size / 1024 / 1024, 2),
|
||||||
|
"modified": file_modified,
|
||||||
|
"error": f"Cannot read shapefile: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"shapefiles": shapefiles,
|
||||||
|
"count": len(shapefiles),
|
||||||
|
"directories": overlay_dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/training/shapefile/{filename}/labels")
|
@app.get("/api/training/shapefile/{filename}/labels")
|
||||||
async def get_shapefile_labels(filename: str):
|
async def get_shapefile_labels(filename: str):
|
||||||
"""Lấy các label từ một shapefile cụ thể"""
|
"""Lấy các label từ một shapefile cụ thể"""
|
||||||
@@ -1729,6 +1814,85 @@ def update_progress(message: str):
|
|||||||
print(f"[PROGRESS] {message}")
|
print(f"[PROGRESS] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize_shapefile_overlay(shapefile_path, reference_raster, boundary_value=255):
|
||||||
|
"""
|
||||||
|
Rasterize shapefile boundaries to overlay on prediction result.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shapefile_path: Path to shapefile
|
||||||
|
reference_raster: xarray DataArray to match dimensions and CRS
|
||||||
|
boundary_value: Value to use for boundaries (default 255 for white)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy array with boundaries, same shape as reference_raster
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import geopandas as gpd
|
||||||
|
from rasterio.features import rasterize
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Read shapefile
|
||||||
|
gdf = gpd.read_file(shapefile_path)
|
||||||
|
print(f"[OVERLAY] Loaded shapefile with {len(gdf)} features, CRS: {gdf.crs}")
|
||||||
|
|
||||||
|
# Ensure CRS matches
|
||||||
|
target_crs = reference_raster.rio.crs
|
||||||
|
if gdf.crs != target_crs:
|
||||||
|
print(f"[OVERLAY] Reprojecting from {gdf.crs} to {target_crs}")
|
||||||
|
gdf = gdf.to_crs(target_crs)
|
||||||
|
|
||||||
|
# Get raster dimensions and transform
|
||||||
|
height, width = reference_raster.shape
|
||||||
|
transform = reference_raster.rio.transform()
|
||||||
|
print(f"[OVERLAY] Raster dimensions: {height}x{width}")
|
||||||
|
print(f"[OVERLAY] Transform: {transform}")
|
||||||
|
|
||||||
|
# Calculate appropriate buffer size based on pixel resolution
|
||||||
|
# Get pixel size from transform (transform[0] is x resolution)
|
||||||
|
pixel_size = abs(transform[0]) # in CRS units
|
||||||
|
# Very thin boundary - only 0.2 pixels wide for 1px line
|
||||||
|
buffer_distance = pixel_size * 0.2
|
||||||
|
print(f"[OVERLAY] Pixel size: {pixel_size}, Buffer distance: {buffer_distance} (thin 1px line)")
|
||||||
|
|
||||||
|
# Create boundary geometries with minimal buffering
|
||||||
|
boundary_geoms = []
|
||||||
|
for idx, geom in enumerate(gdf.geometry):
|
||||||
|
if geom is not None and geom.is_valid:
|
||||||
|
# Get boundary of each polygon
|
||||||
|
boundary = geom.boundary
|
||||||
|
if boundary is not None:
|
||||||
|
# Minimal buffer for 1-pixel thin line
|
||||||
|
buffered = boundary.buffer(buffer_distance)
|
||||||
|
boundary_geoms.append((buffered, boundary_value))
|
||||||
|
|
||||||
|
if not boundary_geoms:
|
||||||
|
print(f"[WARNING] No valid boundary geometries found in {shapefile_path}")
|
||||||
|
return np.zeros((height, width), dtype=np.uint8)
|
||||||
|
|
||||||
|
print(f"[OVERLAY] Rasterizing {len(boundary_geoms)} boundaries...")
|
||||||
|
|
||||||
|
# Rasterize boundaries
|
||||||
|
boundary_mask = rasterize(
|
||||||
|
shapes=boundary_geoms,
|
||||||
|
out_shape=(height, width),
|
||||||
|
transform=transform,
|
||||||
|
fill=0, # Background
|
||||||
|
dtype=np.uint8
|
||||||
|
)
|
||||||
|
|
||||||
|
boundary_count = np.count_nonzero(boundary_mask)
|
||||||
|
print(f"[OVERLAY] Boundary pixels: {boundary_count} / {height*width} ({boundary_count/(height*width)*100:.2f}%)")
|
||||||
|
|
||||||
|
if boundary_count == 0:
|
||||||
|
print(f"[OVERLAY WARNING] No boundary pixels were rasterized! Check CRS and geometry overlap.")
|
||||||
|
|
||||||
|
return boundary_mask
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to rasterize shapefile {shapefile_path}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def update_prediction_progress(message: str):
|
def update_prediction_progress(message: str):
|
||||||
"""Cập nhật prediction progress message"""
|
"""Cập nhật prediction progress message"""
|
||||||
global prediction_status
|
global prediction_status
|
||||||
@@ -2083,6 +2247,33 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
|
|
||||||
prediction_da.rio.to_raster(str(output_file), driver="GTiff")
|
prediction_da.rio.to_raster(str(output_file), driver="GTiff")
|
||||||
|
|
||||||
|
# ============ SHAPEFILE OVERLAY ============
|
||||||
|
overlay_mask = None
|
||||||
|
if config.shapefile_overlay:
|
||||||
|
prediction_status["progress"] = f"Đang overlay shapefile: {config.shapefile_overlay}..."
|
||||||
|
print(f"[OVERLAY] Shapefile overlay requested: {config.shapefile_overlay}")
|
||||||
|
|
||||||
|
# Validate shapefile path exists
|
||||||
|
shapefile_path = Path(config.shapefile_overlay)
|
||||||
|
if not shapefile_path.exists():
|
||||||
|
print(f"[OVERLAY ERROR] Shapefile not found: {shapefile_path}")
|
||||||
|
print(f"[OVERLAY ERROR] Absolute path: {shapefile_path.absolute()}")
|
||||||
|
print(f"[OVERLAY ERROR] Current working directory: {Path.cwd()}")
|
||||||
|
else:
|
||||||
|
print(f"[OVERLAY] Shapefile exists: {shapefile_path.absolute()}")
|
||||||
|
try:
|
||||||
|
overlay_mask = rasterize_shapefile_overlay(str(shapefile_path), prediction_da)
|
||||||
|
if overlay_mask is not None and np.count_nonzero(overlay_mask) > 0:
|
||||||
|
print(f"[OVERLAY] Successfully rasterized shapefile boundaries ({np.count_nonzero(overlay_mask)} pixels)")
|
||||||
|
else:
|
||||||
|
print(f"[OVERLAY WARNING] Shapefile rasterized but no boundary pixels found")
|
||||||
|
except Exception as overlay_error:
|
||||||
|
print(f"[OVERLAY ERROR] Exception: {overlay_error}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
else:
|
||||||
|
print(f"[OVERLAY] No shapefile overlay requested")
|
||||||
|
|
||||||
# Generate PNG preview for web display
|
# Generate PNG preview for web display
|
||||||
prediction_status["progress"] = "Đang tạo PNG preview..."
|
prediction_status["progress"] = "Đang tạo PNG preview..."
|
||||||
png_file = output_dir / f"prediction_{timestamp}.png"
|
png_file = output_dir / f"prediction_{timestamp}.png"
|
||||||
@@ -2097,6 +2288,42 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
|
|
||||||
# Plot prediction with colormap
|
# Plot prediction with colormap
|
||||||
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
|
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
|
||||||
|
|
||||||
|
# Overlay shapefile boundaries if available
|
||||||
|
if overlay_mask is not None and np.count_nonzero(overlay_mask) > 0:
|
||||||
|
print(f"[PNG OVERLAY] Overlaying {np.count_nonzero(overlay_mask)} boundary pixels")
|
||||||
|
|
||||||
|
# Create a mask for boundaries (where overlay_mask > 0)
|
||||||
|
boundary_mask = overlay_mask > 0
|
||||||
|
|
||||||
|
# Method: Direct overlay with high-contrast colors
|
||||||
|
# Create RGBA overlay image
|
||||||
|
overlay_rgba = np.zeros((*predictions_2d.shape, 4))
|
||||||
|
overlay_rgba[boundary_mask, 0] = 1.0 # Red = 1.0 (white)
|
||||||
|
overlay_rgba[boundary_mask, 1] = 1.0 # Green = 1.0 (white)
|
||||||
|
overlay_rgba[boundary_mask, 2] = 1.0 # Blue = 1.0 (white)
|
||||||
|
overlay_rgba[boundary_mask, 3] = 1.0 # Alpha = 1.0 (fully opaque)
|
||||||
|
|
||||||
|
# Overlay on top of prediction
|
||||||
|
ax.imshow(overlay_rgba, interpolation='nearest')
|
||||||
|
|
||||||
|
# Also add a black outline for better contrast
|
||||||
|
from scipy import ndimage
|
||||||
|
boundary_dilated = ndimage.binary_dilation(boundary_mask, iterations=1)
|
||||||
|
boundary_outline = boundary_dilated & ~boundary_mask
|
||||||
|
|
||||||
|
outline_rgba = np.zeros((*predictions_2d.shape, 4))
|
||||||
|
outline_rgba[boundary_outline, 0] = 0.0 # Black outline
|
||||||
|
outline_rgba[boundary_outline, 1] = 0.0
|
||||||
|
outline_rgba[boundary_outline, 2] = 0.0
|
||||||
|
outline_rgba[boundary_outline, 3] = 0.8
|
||||||
|
|
||||||
|
ax.imshow(outline_rgba, interpolation='nearest')
|
||||||
|
|
||||||
|
print(f"[PNG OVERLAY] Added shapefile boundaries to visualization (direct overlay method)")
|
||||||
|
else:
|
||||||
|
print(f"[PNG OVERLAY] No overlay mask or empty mask (pixels: {np.count_nonzero(overlay_mask) if overlay_mask is not None else 0})")
|
||||||
|
|
||||||
ax.set_title(f'Land Classification - {timestamp}', fontsize=16, fontweight='bold', pad=20)
|
ax.set_title(f'Land Classification - {timestamp}', fontsize=16, fontweight='bold', pad=20)
|
||||||
ax.set_xlabel('X (pixels)', fontsize=11)
|
ax.set_xlabel('X (pixels)', fontsize=11)
|
||||||
ax.set_ylabel('Y (pixels)', fontsize=11)
|
ax.set_ylabel('Y (pixels)', fontsize=11)
|
||||||
@@ -2176,7 +2403,9 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
"n_features": features.shape[1],
|
"n_features": features.shape[1],
|
||||||
"feature_mode": feature_mode,
|
"feature_mode": feature_mode,
|
||||||
"used_radar": use_radar,
|
"used_radar": use_radar,
|
||||||
"model_used": config.model_filename
|
"model_used": config.model_filename,
|
||||||
|
"shapefile_overlay": config.shapefile_overlay,
|
||||||
|
"overlay_applied": overlay_mask is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
# Auto generate prediction report
|
# Auto generate prediction report
|
||||||
@@ -4355,8 +4584,69 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from matplotlib.patches import Patch
|
from matplotlib.patches import Patch
|
||||||
|
|
||||||
|
# ============ SHAPEFILE OVERLAY ============
|
||||||
|
overlay_mask = None
|
||||||
|
if config.shapefile_overlay:
|
||||||
|
print(f"[OVERLAY] Shapefile overlay requested: {config.shapefile_overlay}")
|
||||||
|
|
||||||
|
# Validate shapefile path exists
|
||||||
|
shapefile_path = Path(config.shapefile_overlay)
|
||||||
|
if not shapefile_path.exists():
|
||||||
|
print(f"[OVERLAY ERROR] Shapefile not found: {shapefile_path}")
|
||||||
|
print(f"[OVERLAY ERROR] Absolute path: {shapefile_path.absolute()}")
|
||||||
|
else:
|
||||||
|
print(f"[OVERLAY] Shapefile exists: {shapefile_path.absolute()}")
|
||||||
|
try:
|
||||||
|
# Import rioxarray for rio accessor
|
||||||
|
import rioxarray
|
||||||
|
|
||||||
|
# Create temporary DataArray for rasterization
|
||||||
|
temp_da = xr.DataArray(
|
||||||
|
prediction_raster,
|
||||||
|
coords={
|
||||||
|
"y": np.linspace(bbox[3], bbox[1], height),
|
||||||
|
"x": np.linspace(bbox[0], bbox[2], width)
|
||||||
|
},
|
||||||
|
dims=["y", "x"]
|
||||||
|
)
|
||||||
|
temp_da.rio.write_crs("EPSG:4326", inplace=True)
|
||||||
|
temp_da.rio.write_transform(transform, inplace=True)
|
||||||
|
|
||||||
|
overlay_mask = rasterize_shapefile_overlay(str(shapefile_path), temp_da)
|
||||||
|
if overlay_mask is not None and np.count_nonzero(overlay_mask) > 0:
|
||||||
|
print(f"[OVERLAY] Successfully rasterized shapefile boundaries ({np.count_nonzero(overlay_mask)} pixels)")
|
||||||
|
else:
|
||||||
|
print(f"[OVERLAY WARNING] Shapefile rasterized but no boundary pixels found")
|
||||||
|
except Exception as overlay_error:
|
||||||
|
print(f"[OVERLAY ERROR] Exception: {overlay_error}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
else:
|
||||||
|
print(f"[OVERLAY] No shapefile overlay requested")
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(14, 10), dpi=150)
|
fig, ax = plt.subplots(figsize=(14, 10), dpi=150)
|
||||||
im = ax.imshow(prediction_raster, cmap='tab20', interpolation='nearest')
|
im = ax.imshow(prediction_raster, cmap='tab20', interpolation='nearest')
|
||||||
|
|
||||||
|
# Overlay shapefile boundaries if available
|
||||||
|
if overlay_mask is not None and np.count_nonzero(overlay_mask) > 0:
|
||||||
|
print(f"[PNG OVERLAY] Overlaying {np.count_nonzero(overlay_mask)} boundary pixels (1px thin line)")
|
||||||
|
|
||||||
|
# Create a mask for boundaries
|
||||||
|
boundary_mask = overlay_mask > 0
|
||||||
|
|
||||||
|
# Create RGBA overlay image - thin 1px white line only
|
||||||
|
overlay_rgba = np.zeros((*prediction_raster.shape, 4))
|
||||||
|
overlay_rgba[boundary_mask, 0] = 1.0 # White (R=1)
|
||||||
|
overlay_rgba[boundary_mask, 1] = 1.0 # White (G=1)
|
||||||
|
overlay_rgba[boundary_mask, 2] = 1.0 # White (B=1)
|
||||||
|
overlay_rgba[boundary_mask, 3] = 1.0 # Fully opaque
|
||||||
|
|
||||||
|
ax.imshow(overlay_rgba, interpolation='nearest')
|
||||||
|
|
||||||
|
print(f"[PNG OVERLAY] Added thin 1px shapefile boundaries to visualization")
|
||||||
|
else:
|
||||||
|
print(f"[PNG OVERLAY] No overlay mask or empty mask")
|
||||||
|
|
||||||
ax.set_title(f'Land Classification - {timestamp}', fontsize=16, fontweight='bold', pad=20)
|
ax.set_title(f'Land Classification - {timestamp}', fontsize=16, fontweight='bold', pad=20)
|
||||||
ax.set_xlabel('X (pixels)', fontsize=11)
|
ax.set_xlabel('X (pixels)', fontsize=11)
|
||||||
ax.set_ylabel('Y (pixels)', fontsize=11)
|
ax.set_ylabel('Y (pixels)', fontsize=11)
|
||||||
|
|||||||
+190
-1
@@ -774,6 +774,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Shapefile Overlay Option -->
|
||||||
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
|
<label style="font-weight: 600; color: #c2410c; margin-bottom: 8px; display: block;">
|
||||||
|
🗺️ Overlay Shapefile (Hiển thị ranh giới lô đất)
|
||||||
|
</label>
|
||||||
|
<select id="shapefileOverlay" onchange="onShapefileSelected(event)" style="width: 100%; padding: 10px; border: 2px solid #fdba74; border-radius: 8px; font-size: 14px; background: white;">
|
||||||
|
<option value="">-- Không overlay --</option>
|
||||||
|
<!-- Shapefiles will be loaded here -->
|
||||||
|
</select>
|
||||||
|
<div style="font-size: 0.85em; color: #9a3412; margin-top: 4px; line-height: 1.4;">
|
||||||
|
<b>🎯 Tự động cập nhật vùng prediction:</b><br>
|
||||||
|
✅ Khi chọn shapefile → <b>Bbox trên bản đồ tự động thay đổi</b> theo vùng shapefile<br>
|
||||||
|
✅ <b>CRS sẽ tự động chuyển đổi</b> - không cần lo về EPSG:4326/9209/32648<br>
|
||||||
|
💡 Không chọn shapefile → Dùng bbox tùy chỉnh do bạn vẽ trên bản đồ
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary" onclick="startPrediction()" id="predictBtn" style="margin-top: 5px; width: 100%; font-size: 1.1em; padding: 16px;">
|
<button class="btn btn-primary" onclick="startPrediction()" id="predictBtn" style="margin-top: 5px; width: 100%; font-size: 1.1em; padding: 16px;">
|
||||||
@@ -1651,6 +1668,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get shapefile overlay option
|
||||||
|
const shapefileOverlay = document.getElementById('shapefileOverlay').value;
|
||||||
|
|
||||||
|
// Warn if no shapefile selected (optional but recommended)
|
||||||
|
if (!shapefileOverlay) {
|
||||||
|
const confirmWithoutShapefile = confirm(
|
||||||
|
'⚠️ CẢNH BÁO: Bạn chưa chọn shapefile!\n\n' +
|
||||||
|
'❌ Kết quả sẽ KHÔNG có đường ranh giới lô đất.\n\n' +
|
||||||
|
'💡 Để có đường phân lô trên ảnh kết quả:\n' +
|
||||||
|
' - Hủy bỏ\n' +
|
||||||
|
' - Chọn shapefile trong dropdown "Overlay Shapefile"\n' +
|
||||||
|
' - Chạy lại prediction\n\n' +
|
||||||
|
'Bạn có muốn tiếp tục KHÔNG CÓ ranh giới lô đất không?'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmWithoutShapefile) {
|
||||||
|
console.log('[PREDICTION] User cancelled to select shapefile');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`[PREDICTION] Shapefile selected: ${shapefileOverlay}`);
|
||||||
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
model_filename: modelFilename,
|
model_filename: modelFilename,
|
||||||
min_lon: selectedBbox.min_lon,
|
min_lon: selectedBbox.min_lon,
|
||||||
@@ -1666,7 +1706,8 @@
|
|||||||
export_ndvi: exportNDVI,
|
export_ndvi: exportNDVI,
|
||||||
export_classification: true,
|
export_classification: true,
|
||||||
cloud_removal_method: cloudRemovalConfig.method,
|
cloud_removal_method: cloudRemovalConfig.method,
|
||||||
cloud_removal_model: cloudRemovalConfig.model_filename || null
|
cloud_removal_model: cloudRemovalConfig.model_filename || null,
|
||||||
|
shapefile_overlay: shapefileOverlay || null
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -2598,6 +2639,7 @@
|
|||||||
loadPredProvinces(); // Load provinces list
|
loadPredProvinces(); // Load provinces list
|
||||||
loadNDVIProvinces(); // Load NDVI provinces list
|
loadNDVIProvinces(); // Load NDVI provinces list
|
||||||
loadNDVIModels(); // Load models for NDVI
|
loadNDVIModels(); // Load models for NDVI
|
||||||
|
loadOverlayShapefiles(); // Load shapefiles for overlay
|
||||||
|
|
||||||
// Add event listener for model selection
|
// Add event listener for model selection
|
||||||
document.getElementById('modelSelect').addEventListener('change', updateModelInfo);
|
document.getElementById('modelSelect').addEventListener('change', updateModelInfo);
|
||||||
@@ -2993,6 +3035,153 @@
|
|||||||
|
|
||||||
alert(`✅ Đã áp dụng preset: ${config.name}\n\nBbox: [${config.bbox.join(', ')}]\nThời gian: ${config.start_date} → ${config.end_date}\nSample points: ${config.sample_points}`);
|
alert(`✅ Đã áp dụng preset: ${config.name}\n\nBbox: [${config.bbox.join(', ')}]\nThời gian: ${config.start_date} → ${config.end_date}\nSample points: ${config.sample_points}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load overlay shapefiles
|
||||||
|
async function loadOverlayShapefiles() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/overlay/shapefiles');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const select = document.getElementById('shapefileOverlay');
|
||||||
|
select.innerHTML = '<option value="">-- Không overlay --</option>';
|
||||||
|
|
||||||
|
if (data.shapefiles && data.shapefiles.length > 0) {
|
||||||
|
data.shapefiles.forEach(shp => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = shp.path;
|
||||||
|
|
||||||
|
// Build detailed label with CRS and bbox info
|
||||||
|
let label = `${shp.filename} - ${shp.feature_count} features`;
|
||||||
|
|
||||||
|
// Add CRS info (important for matching!)
|
||||||
|
if (shp.crs) {
|
||||||
|
const crsCode = shp.crs.split(':').pop(); // Extract code from "EPSG:4326"
|
||||||
|
label += ` | CRS: ${crsCode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add bbox info for easy matching with prediction area
|
||||||
|
if (shp.bbox && shp.bbox.length === 4) {
|
||||||
|
const [minLon, minLat, maxLon, maxLat] = shp.bbox;
|
||||||
|
label += ` | Vùng: [${minLon.toFixed(2)}, ${minLat.toFixed(2)}, ${maxLon.toFixed(2)}, ${maxLat.toFixed(2)}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
option.textContent = label;
|
||||||
|
|
||||||
|
// Store full shapefile info as data attributes for later use
|
||||||
|
option.dataset.crs = shp.crs || '';
|
||||||
|
option.dataset.bbox = JSON.stringify(shp.bbox || []);
|
||||||
|
option.dataset.featureCount = shp.feature_count;
|
||||||
|
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Overlay Shapefiles] Loaded ${data.shapefiles.length} shapefiles`);
|
||||||
|
} else {
|
||||||
|
console.log('[Overlay Shapefiles] No shapefiles found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for shapefile selection change (OUTSIDE the if block)
|
||||||
|
// Remove old listener first to prevent duplicates
|
||||||
|
select.removeEventListener('change', onShapefileSelected);
|
||||||
|
select.addEventListener('change', onShapefileSelected);
|
||||||
|
console.log('[Overlay Shapefiles] Event listener attached');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Overlay Shapefiles] Error loading shapefiles:', error);
|
||||||
|
const select = document.getElementById('shapefileOverlay');
|
||||||
|
select.innerHTML = '<option value="">Error loading shapefiles</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle shapefile selection - auto update bbox on map
|
||||||
|
function onShapefileSelected(event) {
|
||||||
|
console.log('[Shapefile Select] Event triggered');
|
||||||
|
console.log('[Shapefile Select] map exists:', typeof map !== 'undefined');
|
||||||
|
console.log('[Shapefile Select] drawnItems exists:', typeof drawnItems !== 'undefined');
|
||||||
|
|
||||||
|
const selectedOption = event.target.selectedOptions[0];
|
||||||
|
|
||||||
|
// If no shapefile selected (empty value), keep current bbox
|
||||||
|
if (!selectedOption || !selectedOption.value) {
|
||||||
|
console.log('[Shapefile Select] No shapefile selected, keeping current bbox');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Shapefile Select] Selected shapefile:', selectedOption.value);
|
||||||
|
|
||||||
|
// Get bbox from data attribute
|
||||||
|
const bboxData = selectedOption.dataset.bbox;
|
||||||
|
console.log('[Shapefile Select] Bbox data:', bboxData);
|
||||||
|
|
||||||
|
if (!bboxData || bboxData === '[]') {
|
||||||
|
console.warn('[Shapefile Select] Selected shapefile has no bbox data');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bbox = JSON.parse(bboxData);
|
||||||
|
console.log('[Shapefile Select] Parsed bbox:', bbox);
|
||||||
|
|
||||||
|
if (bbox.length !== 4) {
|
||||||
|
console.warn('[Shapefile Select] Invalid bbox format:', bbox);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [minLon, minLat, maxLon, maxLat] = bbox;
|
||||||
|
|
||||||
|
// Validate bbox
|
||||||
|
if (minLon < -180 || maxLon > 180 || minLat < -90 || maxLat > 90) {
|
||||||
|
alert('❌ Bbox của shapefile không hợp lệ!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Shapefile Select] Creating rectangle with bounds:', [[minLat, minLon], [maxLat, maxLon]]);
|
||||||
|
|
||||||
|
// Update prediction map bbox
|
||||||
|
const bounds = [
|
||||||
|
[minLat, minLon],
|
||||||
|
[maxLat, maxLon]
|
||||||
|
];
|
||||||
|
|
||||||
|
const rectangle = L.rectangle(bounds, {
|
||||||
|
color: '#667eea',
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear old bbox and add new one
|
||||||
|
console.log('[Shapefile Select] Clearing old layers...');
|
||||||
|
drawnItems.clearLayers();
|
||||||
|
console.log('[Shapefile Select] Adding new rectangle...');
|
||||||
|
drawnItems.addLayer(rectangle);
|
||||||
|
console.log('[Shapefile Select] Fitting map to bounds...');
|
||||||
|
map.fitBounds(bounds, { padding: [50, 50] });
|
||||||
|
|
||||||
|
// Update selected bbox variable
|
||||||
|
selectedBbox = {
|
||||||
|
min_lon: minLon,
|
||||||
|
min_lat: minLat,
|
||||||
|
max_lon: maxLon,
|
||||||
|
max_lat: maxLat
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
localStorage.setItem('prediction_bbox', JSON.stringify(selectedBbox));
|
||||||
|
|
||||||
|
console.log(`[Shapefile Select] Auto-updated bbox from shapefile:`, selectedBbox);
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
const crs = selectedOption.dataset.crs || 'Unknown';
|
||||||
|
alert(`✅ Đã tự động cập nhật vùng prediction theo shapefile!\n\n` +
|
||||||
|
`📍 Bbox: [${minLon.toFixed(4)}, ${minLat.toFixed(4)}, ${maxLon.toFixed(4)}, ${maxLat.toFixed(4)}]\n` +
|
||||||
|
`🗺️ CRS: ${crs}\n\n` +
|
||||||
|
`💡 Bạn có thể điều chỉnh lại bằng cách vẽ lại trên bản đồ nếu muốn.`);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Shapefile Select] Error parsing bbox:', e);
|
||||||
|
alert(`❌ Lỗi khi xử lý bbox: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify shapefile overlay API returns correct bbox data
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
def test_shapefile_api():
|
||||||
|
"""Test /api/overlay/shapefiles endpoint"""
|
||||||
|
print("Testing /api/overlay/shapefiles endpoint...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get('http://localhost:8000/api/overlay/shapefiles')
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"\n✅ API Response successful")
|
||||||
|
print(f"Total shapefiles: {data.get('count', 0)}")
|
||||||
|
|
||||||
|
if data.get('shapefiles'):
|
||||||
|
print("\n📋 Shapefile details:")
|
||||||
|
for idx, shp in enumerate(data['shapefiles'], 1):
|
||||||
|
print(f"\n{idx}. {shp.get('filename')}")
|
||||||
|
print(f" Path: {shp.get('path')}")
|
||||||
|
print(f" CRS: {shp.get('crs')}")
|
||||||
|
print(f" Features: {shp.get('feature_count')}")
|
||||||
|
print(f" Bbox: {shp.get('bbox')}")
|
||||||
|
|
||||||
|
# Verify bbox format
|
||||||
|
bbox = shp.get('bbox')
|
||||||
|
if bbox and len(bbox) == 4:
|
||||||
|
print(f" ✅ Bbox format valid: [minLon, minLat, maxLon, maxLat]")
|
||||||
|
else:
|
||||||
|
print(f" ❌ Bbox format invalid or missing!")
|
||||||
|
else:
|
||||||
|
print("\n⚠️ No shapefiles found")
|
||||||
|
else:
|
||||||
|
print(f"\n❌ API returned status code: {response.status_code}")
|
||||||
|
print(f"Response: {response.text}")
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("\n❌ Cannot connect to API server. Is it running on localhost:8000?")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_shapefile_api()
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Test script to verify shapefile overlay functionality
|
||||||
|
"""
|
||||||
|
import geopandas as gpd
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Test shapefile path
|
||||||
|
shapefile_path = "ChauThanh/HienTrang/ChauThanh_kiemke.shp"
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("TESTING SHAPEFILE OVERLAY")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# Check if file exists
|
||||||
|
shp = Path(shapefile_path)
|
||||||
|
print(f"\n1. Checking file existence:")
|
||||||
|
print(f" Path: {shp}")
|
||||||
|
print(f" Exists: {shp.exists()}")
|
||||||
|
print(f" Absolute: {shp.absolute()}")
|
||||||
|
|
||||||
|
if shp.exists():
|
||||||
|
# Read shapefile
|
||||||
|
print(f"\n2. Reading shapefile...")
|
||||||
|
gdf = gpd.read_file(str(shp))
|
||||||
|
print(f" Features: {len(gdf)}")
|
||||||
|
print(f" CRS: {gdf.crs}")
|
||||||
|
print(f" Bounds: {gdf.total_bounds}")
|
||||||
|
print(f" Columns: {list(gdf.columns)}")
|
||||||
|
|
||||||
|
# Check geometries
|
||||||
|
print(f"\n3. Checking geometries...")
|
||||||
|
valid_count = sum(1 for geom in gdf.geometry if geom is not None and geom.is_valid)
|
||||||
|
print(f" Valid geometries: {valid_count} / {len(gdf)}")
|
||||||
|
|
||||||
|
# Sample geometry bounds
|
||||||
|
if len(gdf) > 0:
|
||||||
|
sample_geom = gdf.geometry.iloc[0]
|
||||||
|
print(f" Sample geometry type: {sample_geom.geom_type}")
|
||||||
|
print(f" Sample geometry bounds: {sample_geom.bounds}")
|
||||||
|
|
||||||
|
# Test reprojection to EPSG:4326
|
||||||
|
print(f"\n4. Testing reprojection to EPSG:4326...")
|
||||||
|
try:
|
||||||
|
gdf_4326 = gdf.to_crs("EPSG:4326")
|
||||||
|
print(f" Success!")
|
||||||
|
print(f" New bounds: {gdf_4326.total_bounds}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: {e}")
|
||||||
|
|
||||||
|
# Test boundary extraction
|
||||||
|
print(f"\n5. Testing boundary extraction...")
|
||||||
|
boundaries = []
|
||||||
|
for geom in gdf.geometry:
|
||||||
|
if geom is not None and geom.is_valid:
|
||||||
|
boundary = geom.boundary
|
||||||
|
if boundary is not None:
|
||||||
|
boundaries.append(boundary)
|
||||||
|
print(f" Extracted boundaries: {len(boundaries)}")
|
||||||
|
|
||||||
|
# Test buffering
|
||||||
|
print(f"\n6. Testing buffer...")
|
||||||
|
buffer_size = 0.001 # degrees or meters depending on CRS
|
||||||
|
buffered = []
|
||||||
|
for boundary in boundaries[:10]: # Test first 10
|
||||||
|
try:
|
||||||
|
buf = boundary.buffer(buffer_size)
|
||||||
|
buffered.append(buf)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Buffer error: {e}")
|
||||||
|
print(f" Successfully buffered: {len(buffered)} / 10")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(" ERROR: Shapefile not found!")
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("TEST COMPLETE")
|
||||||
|
print("=" * 70)
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Test Shapefile Selection</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||||
|
#map { height: 400px; border: 2px solid #ccc; margin: 20px 0; }
|
||||||
|
.info-box { background: #f0f0f0; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🧪 Test Shapefile Auto-Select Bbox</h1>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>Chọn Shapefile:</h3>
|
||||||
|
<select id="shapefileOverlay" onchange="onShapefileSelected(event)" style="width: 100%; padding: 10px; font-size: 14px;">
|
||||||
|
<option value="">-- Chọn shapefile --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>Current Bbox:</h3>
|
||||||
|
<pre id="bboxInfo">Chưa chọn shapefile</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>Console Logs:</h3>
|
||||||
|
<pre id="console" style="max-height: 200px; overflow-y: auto; background: #000; color: #0f0; padding: 10px;"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script>
|
||||||
|
// Global variables
|
||||||
|
let map, drawnItems, selectedBbox = null;
|
||||||
|
|
||||||
|
// Custom console.log to display in page
|
||||||
|
const originalLog = console.log;
|
||||||
|
console.log = function(...args) {
|
||||||
|
originalLog.apply(console, args);
|
||||||
|
const consoleEl = document.getElementById('console');
|
||||||
|
consoleEl.textContent += args.join(' ') + '\n';
|
||||||
|
consoleEl.scrollTop = consoleEl.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize map
|
||||||
|
function initMap() {
|
||||||
|
map = L.map('map').setView([10.0, 105.8], 10);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
drawnItems = new L.FeatureGroup();
|
||||||
|
map.addLayer(drawnItems);
|
||||||
|
|
||||||
|
console.log('✅ Map initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load shapefiles from API
|
||||||
|
async function loadShapefiles() {
|
||||||
|
try {
|
||||||
|
console.log('📡 Fetching shapefiles from API...');
|
||||||
|
const response = await fetch('http://localhost:8000/api/overlay/shapefiles');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const select = document.getElementById('shapefileOverlay');
|
||||||
|
select.innerHTML = '<option value="">-- Chọn shapefile --</option>';
|
||||||
|
|
||||||
|
if (data.shapefiles && data.shapefiles.length > 0) {
|
||||||
|
data.shapefiles.forEach(shp => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = shp.path;
|
||||||
|
|
||||||
|
let label = `${shp.filename} - ${shp.feature_count} features`;
|
||||||
|
if (shp.crs) {
|
||||||
|
const crsCode = shp.crs.split(':').pop();
|
||||||
|
label += ` | CRS: ${crsCode}`;
|
||||||
|
}
|
||||||
|
if (shp.bbox && shp.bbox.length === 4) {
|
||||||
|
const [minLon, minLat, maxLon, maxLat] = shp.bbox;
|
||||||
|
label += ` | [${minLon.toFixed(2)}, ${minLat.toFixed(2)}, ${maxLon.toFixed(2)}, ${maxLat.toFixed(2)}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
option.textContent = label;
|
||||||
|
option.dataset.crs = shp.crs || '';
|
||||||
|
option.dataset.bbox = JSON.stringify(shp.bbox || []);
|
||||||
|
option.dataset.featureCount = shp.feature_count;
|
||||||
|
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Loaded ${data.shapefiles.length} shapefiles`);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ No shapefiles found');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error loading shapefiles:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle shapefile selection
|
||||||
|
function onShapefileSelected(event) {
|
||||||
|
console.log('🔔 Shapefile selection changed');
|
||||||
|
|
||||||
|
const selectedOption = event.target.selectedOptions[0];
|
||||||
|
|
||||||
|
if (!selectedOption || !selectedOption.value) {
|
||||||
|
console.log('ℹ️ No shapefile selected');
|
||||||
|
document.getElementById('bboxInfo').textContent = 'Chưa chọn shapefile';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bboxData = selectedOption.dataset.bbox;
|
||||||
|
console.log('📦 Bbox data from option:', bboxData);
|
||||||
|
|
||||||
|
if (!bboxData || bboxData === '[]') {
|
||||||
|
console.log('⚠️ No bbox data in selected option');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bbox = JSON.parse(bboxData);
|
||||||
|
console.log('📊 Parsed bbox:', bbox);
|
||||||
|
|
||||||
|
if (bbox.length !== 4) {
|
||||||
|
console.log('❌ Invalid bbox length:', bbox.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [minLon, minLat, maxLon, maxLat] = bbox;
|
||||||
|
|
||||||
|
// Validate bbox
|
||||||
|
if (minLon < -180 || maxLon > 180 || minLat < -90 || maxLat > 90) {
|
||||||
|
console.log('❌ Bbox out of valid range');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Valid bbox:', {minLon, minLat, maxLon, maxLat});
|
||||||
|
|
||||||
|
// Update map
|
||||||
|
const bounds = [[minLat, minLon], [maxLat, maxLon]];
|
||||||
|
const rectangle = L.rectangle(bounds, {
|
||||||
|
color: '#667eea',
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
});
|
||||||
|
|
||||||
|
drawnItems.clearLayers();
|
||||||
|
drawnItems.addLayer(rectangle);
|
||||||
|
map.fitBounds(bounds, { padding: [50, 50] });
|
||||||
|
|
||||||
|
selectedBbox = {min_lon: minLon, min_lat: minLat, max_lon: maxLon, max_lat: maxLat};
|
||||||
|
|
||||||
|
console.log('🗺️ Map updated with new bbox');
|
||||||
|
|
||||||
|
// Update bbox info display
|
||||||
|
document.getElementById('bboxInfo').textContent = JSON.stringify(selectedBbox, null, 2);
|
||||||
|
|
||||||
|
alert(`✅ Bbox updated!\n\nmin_lon: ${minLon.toFixed(4)}\nmin_lat: ${minLat.toFixed(4)}\nmax_lon: ${maxLon.toFixed(4)}\nmax_lat: ${maxLat.toFixed(4)}`);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ Error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on load
|
||||||
|
window.onload = function() {
|
||||||
|
console.log('🚀 Page loaded, initializing...');
|
||||||
|
initMap();
|
||||||
|
loadShapefiles();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user