cơ bản hoàn tát các chức năng chính

This commit is contained in:
Victor Phan
2025-12-21 17:31:51 +07:00
parent 82aa814777
commit 46da481029
15 changed files with 5470 additions and 523 deletions
+9 -439
View File
@@ -286,7 +286,7 @@
<body>
<div class="container">
<div class="header">
<h1>🌍 Land Classification Training</h1>
<h1>Training Interface</h1>
<p>Giao diện training model phân loại đất từ ảnh vệ tinh</p>
</div>
@@ -507,6 +507,10 @@
<div id="modelsList" class="model-list" style="margin-top: 15px;">
<p>Đang tải...</p>
</div>
<div style="margin-top: 15px; visibility: hidden;">
<label for="selectedModel" style="font-weight:600; color:#667eea;">Chọn model để dự đoán:</label>
<select id="selectedModel" style="width:100%; padding:10px; border-radius:5px; font-size:1em; margin-top:5px;"></select>
</div>
</div>
<!-- Reports Section -->
@@ -518,114 +522,7 @@
</div>
</div>
<!-- Prediction Section -->
<div class="section" style="grid-column: 1 / -1;">
<h2 style="text-align: center; margin-bottom: 30px;">🔮 Dự Đoán & Phân Loại (Prediction & Classification)</h2>
<div style="display: grid; grid-template-columns: 1.2fr 1fr; gap: 30px;">
<!-- Left: Prediction Map -->
<div>
<div style="background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%); padding: 20px; border-radius: 12px; border: 2px solid #ff6b6b40;">
<h3 style="margin: 0 0 15px 0; color: #ff6b6b; font-size: 18px;">🗺️ Bản Đồ Khu Vực Dự Đoán</h3>
<div class="map-instructions" style="background: #fff3cd; border-left: 4px solid #ff6b6b; margin-bottom: 15px;">
<strong>💡 Hướng dẫn:</strong> Sử dụng công cụ vẽ hình chữ nhật
<span style="display: inline-block; width: 24px; height: 24px; background: white; border: 2px solid #ff6b6b; vertical-align: middle; margin: 0 5px;"></span>
để chọn khu vực cần dự đoán
</div>
<div id="predictionMap" style="height: 600px; border-radius: 8px; border: 3px solid #ff6b6b; box-shadow: 0 4px 12px rgba(255,107,107,0.3);"></div>
<div style="margin-top: 15px; padding: 12px; background: white; border-radius: 6px; border: 1px solid #ddd;">
<strong style="color: #ff6b6b;">📍 Tọa độ khu vực:</strong><br>
<span id="predBboxDisplay" style="font-family: monospace; color: #333; font-size: 13px;">Chưa chọn khu vực</span>
</div>
</div>
</div>
<!-- Right: Configuration & Controls -->
<div>
<!-- Prediction Status -->
<div id="predictionStatusBox" class="status-box" style="margin-bottom: 20px;">
<p><strong>Trạng thái:</strong> <span id="predictionStatusText">Chưa bắt đầu</span></p>
<p><strong>Tiến độ:</strong> <span id="predictionProgressText">-</span></p>
</div>
<!-- Prediction Configuration -->
<form id="predictionForm">
<h3 style="margin-bottom: 15px; color: #ff6b6b;">🤖 Chọn Model</h3>
<div class="form-group">
<label>Model để sử dụng:</label>
<select id="selectedModel" required style="border-color: #ff6b6b;">
<option value="">-- Chọn model --</option>
</select>
</div>
<!-- Hidden inputs for prediction bbox -->
<input type="hidden" id="predMinLon" value="105.6" required>
<input type="hidden" id="predMinLat" value="9.3" required>
<input type="hidden" id="predMaxLon" value="106.2" required>
<input type="hidden" id="predMaxLat" value="9.8" required>
<h3 style="margin: 20px 0 15px; color: #ff6b6b;">📅 Thời Gian Dự Đoán</h3>
<div class="form-row">
<div class="form-group">
<label>Ngày bắt đầu:</label>
<input type="date" id="predStartDate" value="2023-03-01" required>
</div>
<div class="form-group">
<label>Ngày kết thúc:</label>
<input type="date" id="predEndDate" value="2023-05-31" required>
</div>
</div>
<h3 style="margin: 20px 0 15px; color: #ff6b6b;">🛰️ Dữ Liệu Vệ Tinh</h3>
<div class="form-row">
<div class="form-group">
<label>Số scenes tối đa:</label>
<input type="number" id="predMaxScenes" value="12" min="1" max="100" required>
</div>
<div class="form-group">
<label>Cloud cover (%):</label>
<input type="number" id="predCloudCover" value="30" min="0" max="100" required>
</div>
</div>
<div class="form-group">
<label>Độ phân giải (m):</label>
<select id="predResolution" required>
<option value="10">10m (Chính xác cao)</option>
<option value="20" selected>20m (Cân bằng)</option>
<option value="30">30m (Nhanh)</option>
</select>
</div>
<div style="margin-top: 30px; text-align: center;">
<button type="submit" class="btn btn-primary" id="predictBtn" style="background: linear-gradient(135deg, #ff6b6b, #ee5a6f); width: 100%; padding: 15px; font-size: 16px; font-weight: 600;">
🔮 Bắt Đầu Dự Đoán
</button>
</div>
</form>
<!-- Prediction Result -->
<div id="predictionResult" style="margin-top: 20px; display: none;">
<h3 style="color: #28a745; margin-bottom: 10px;">✅ Kết Quả Dự Đoán</h3>
<div style="background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); padding: 20px; border-radius: 8px; border: 2px solid #28a745;">
<div id="predResultText" style="font-size: 14px; line-height: 1.8;"></div>
<div id="downloadLinkContainer" style="margin-top: 15px; text-align: center;"></div>
</div>
</div>
<!-- Previous Predictions List -->
<div id="predictionsListSection" style="margin-top: 20px;">
<h3 style="color: #ff6b6b; margin-bottom: 10px;">📂 Các File Dự Đoán Đã Tạo</h3>
<div id="predictionsList" style="background: #f8f9fa; padding: 15px; border-radius: 8px; max-height: 200px; overflow-y: auto;">
<p style="color: #666; text-align: center;">Đang tải...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -644,15 +541,15 @@
times: []
};
// Load presets on page load
window.onload = async () => {
// Load presets and models only after DOM is ready
document.addEventListener('DOMContentLoaded', async () => {
await loadPresets();
await loadModels();
await loadReports();
await loadSystemInfo();
checkStatus();
loadTrainingHistory();
};
});
// Load preset configurations
async function loadPresets() {
@@ -1387,333 +1284,6 @@
document.getElementById('cachePreset').addEventListener('change', applyCachePreset);
});
// ============== PREDICTION FUNCTIONALITY ==============
let predictionMap, predictionDrawnItems, predictionRectangle;
let predictionStatusInterval = null;
// Initialize prediction map
function initPredictionMap() {
predictionMap = L.map('predictionMap').setView([9.55, 105.9], 9);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 18
}).addTo(predictionMap);
predictionDrawnItems = new L.FeatureGroup();
predictionMap.addLayer(predictionDrawnItems);
const drawControl = new L.Control.Draw({
draw: {
polyline: false,
polygon: false,
circle: false,
marker: false,
circlemarker: false,
rectangle: {
shapeOptions: {
color: '#ff6b6b',
weight: 3,
fillOpacity: 0.2
}
}
},
edit: {
featureGroup: predictionDrawnItems,
remove: true
}
});
predictionMap.addControl(drawControl);
predictionMap.on(L.Draw.Event.CREATED, function(event) {
const layer = event.layer;
if (predictionRectangle) {
predictionDrawnItems.removeLayer(predictionRectangle);
}
predictionDrawnItems.addLayer(layer);
predictionRectangle = layer;
const bounds = layer.getBounds();
updatePredictionBbox(bounds);
});
predictionMap.on(L.Draw.Event.EDITED, function(event) {
const layers = event.layers;
layers.eachLayer(function(layer) {
const bounds = layer.getBounds();
updatePredictionBbox(bounds);
});
});
predictionMap.on(L.Draw.Event.DELETED, function() {
predictionRectangle = null;
document.getElementById('predBboxDisplay').textContent = 'Chưa chọn khu vực';
document.getElementById('predMinLon').value = '';
document.getElementById('predMinLat').value = '';
document.getElementById('predMaxLon').value = '';
document.getElementById('predMaxLat').value = '';
});
drawInitialPredictionRectangle();
}
function updatePredictionBbox(bounds) {
const south = bounds.getSouth().toFixed(6);
const west = bounds.getWest().toFixed(6);
const north = bounds.getNorth().toFixed(6);
const east = bounds.getEast().toFixed(6);
document.getElementById('predMinLat').value = south;
document.getElementById('predMinLon').value = west;
document.getElementById('predMaxLat').value = north;
document.getElementById('predMaxLon').value = east;
document.getElementById('predBboxDisplay').textContent =
`Lon: ${west}${east}, Lat: ${south}${north}`;
}
function drawInitialPredictionRectangle() {
const minLon = parseFloat(document.getElementById('predMinLon').value);
const minLat = parseFloat(document.getElementById('predMinLat').value);
const maxLon = parseFloat(document.getElementById('predMaxLon').value);
const maxLat = parseFloat(document.getElementById('predMaxLat').value);
if (minLon && minLat && maxLon && maxLat) {
const bounds = [[minLat, minLon], [maxLat, maxLon]];
const rectangle = L.rectangle(bounds, {
color: '#ff6b6b',
weight: 3,
fillOpacity: 0.2
});
predictionDrawnItems.addLayer(rectangle);
predictionRectangle = rectangle;
predictionMap.fitBounds(bounds);
updatePredictionBbox(L.latLngBounds(bounds));
}
}
// Handle prediction form submission
document.getElementById('predictionForm').onsubmit = async (e) => {
e.preventDefault();
const config = {
model_filename: document.getElementById('selectedModel').value,
min_lon: parseFloat(document.getElementById('predMinLon').value),
min_lat: parseFloat(document.getElementById('predMinLat').value),
max_lon: parseFloat(document.getElementById('predMaxLon').value),
max_lat: parseFloat(document.getElementById('predMaxLat').value),
start_date: document.getElementById('predStartDate').value,
end_date: document.getElementById('predEndDate').value,
max_scenes: parseInt(document.getElementById('predMaxScenes').value),
cloud_cover: parseInt(document.getElementById('predCloudCover').value),
resolution: parseInt(document.getElementById('predResolution').value)
};
if (!config.model_filename) {
alert('Vui lòng chọn model để dự đoán!');
return;
}
try {
const response = await fetch(`${API_BASE}/prediction/start`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
if (!response.ok) {
const error = await response.json();
alert('Lỗi: ' + error.detail);
return;
}
const result = await response.json();
alert(result.message);
// Start monitoring prediction status
if (predictionStatusInterval) clearInterval(predictionStatusInterval);
predictionStatusInterval = setInterval(checkPredictionStatus, 2000);
document.getElementById('predictBtn').disabled = true;
document.getElementById('predictionResult').style.display = 'none';
} catch (error) {
alert('Lỗi kết nối: ' + error.message);
}
};
// Check prediction status
async function checkPredictionStatus() {
try {
const response = await fetch(`${API_BASE}/prediction/status`);
const status = await response.json();
const statusBox = document.getElementById('predictionStatusBox');
const statusText = document.getElementById('predictionStatusText');
const progressText = document.getElementById('predictionProgressText');
statusText.textContent = status.is_predicting ? 'Đang dự đoán...' :
(status.error ? 'Lỗi' : (status.result ? 'Hoàn thành' : 'Chờ'));
progressText.textContent = status.progress || '-';
// Update status box styling
statusBox.className = 'status-box';
if (status.is_predicting) {
statusBox.classList.add('training');
} else if (status.error) {
statusBox.classList.add('error');
} else if (status.result) {
statusBox.classList.add('success');
}
// Enable/disable button
if (!status.is_predicting) {
document.getElementById('predictBtn').disabled = false;
if (predictionStatusInterval) {
clearInterval(predictionStatusInterval);
predictionStatusInterval = null;
}
if (status.result) {
displayPredictionResult(status.result);
}
}
} catch (error) {
console.error('Error checking prediction status:', error);
}
}
// Display prediction result
function displayPredictionResult(result) {
const resultDiv = document.getElementById('predictionResult');
const resultText = document.getElementById('predResultText');
// Store result globally for download/view functions
window.lastPredictionResult = result;
// Extract filename from path
const filename = result.output_file.split('/').pop();
const downloadUrl = `${API_BASE}/predictions/download/${filename}`;
resultText.innerHTML = `
<div style="margin-bottom: 10px;">
<strong>📁 File kết quả:</strong><br>
<code style="background: #fff; padding: 5px 10px; border-radius: 4px; display: inline-block; margin-top: 5px;">${result.output_file}</code>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px;">
<div><strong>📏 Kích thước:</strong> ${result.shape[0]} x ${result.shape[1]} pixels</div>
<div><strong>🎨 Các lớp:</strong> ${result.unique_classes.join(', ')}</div>
<div style="grid-column: 1 / -1;"><strong>📍 Khu vực:</strong> [${result.bbox.map(v => v.toFixed(4)).join(', ')}]</div>
<div style="grid-column: 1 / -1;"><strong>⏰ Thời gian:</strong> ${result.time_range}</div>
</div>
`;
// Add download link
const downloadContainer = document.getElementById('downloadLinkContainer');
downloadContainer.innerHTML = `
<a href="${downloadUrl}"
class="btn btn-primary"
style="background: #28a745; padding: 12px 24px; text-decoration: none; display: inline-block; margin-right: 10px;"
download="${filename}">
📥 Tải GeoTIFF
</a>
<button onclick="viewPredictionResult()" class="btn btn-secondary" style="background: #17a2b8;">
👁️ Xem Chi Tiết
</button>
<div style="margin-top: 10px; font-size: 12px; color: #666;">
Hoặc copy link: <a href="${downloadUrl}" target="_blank" style="color: #28a745;">${downloadUrl}</a>
</div>
`;
resultDiv.style.display = 'block';
// Refresh predictions list
loadPredictionsList();
}
// Download prediction result
function downloadPredictionResult() {
if (window.lastPredictionResult) {
const result = window.lastPredictionResult;
const filename = result.output_file.split('/').pop();
const downloadUrl = `${API_BASE}/predictions/download/${filename}`;
window.open(downloadUrl, '_blank');
} else {
alert('Chưa có kết quả dự đoán nào!');
}
}
// Load list of previous predictions
async function loadPredictionsList() {
try {
const response = await fetch(`${API_BASE}/predictions/list`);
const data = await response.json();
const listDiv = document.getElementById('predictionsList');
if (data.predictions && data.predictions.length > 0) {
listDiv.innerHTML = data.predictions.map(pred => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px; margin-bottom: 8px; background: white; border-radius: 6px; border: 1px solid #ddd;">
<div style="flex: 1;">
<strong style="color: #333;">📄 ${pred.filename}</strong>
<div style="font-size: 12px; color: #666; margin-top: 3px;">
📅 ${new Date(pred.created).toLocaleString('vi-VN')} | 💾 ${pred.size_mb} MB
</div>
</div>
<a href="${pred.download_url}"
class="btn btn-secondary"
style="background: #28a745; padding: 6px 12px; font-size: 12px; text-decoration: none;"
download="${pred.filename}">
📥 Tải về
</a>
</div>
`).join('');
} else {
listDiv.innerHTML = '<p style="color: #666; text-align: center;">Chưa có file dự đoán nào.</p>';
}
} catch (error) {
console.error('Error loading predictions list:', error);
document.getElementById('predictionsList').innerHTML =
'<p style="color: #dc3545; text-align: center;">Lỗi tải danh sách: ' + error.message + '</p>';
}
}
// View prediction result details
function viewPredictionResult() {
if (window.lastPredictionResult) {
const result = window.lastPredictionResult;
const details = `
=== CHI TIẾT KẾT QUẢ DỰ ĐOÁN ===
📁 File Output: ${result.output_file}
📊 Thông số ảnh:
- Kích thước: ${result.shape[0]} x ${result.shape[1]} pixels
- Tổng số pixels: ${result.shape[0] * result.shape[1]}
🎨 Phân loại:
- Các lớp tìm thấy: ${result.unique_classes.join(', ')}
- Số lớp phân biệt: ${result.unique_classes.length}
📍 Vị trí địa lý:
- Bbox: [${result.bbox.map(v => v.toFixed(6)).join(', ')}]
- Min Lon: ${result.bbox[0].toFixed(6)}°
- Min Lat: ${result.bbox[1].toFixed(6)}°
- Max Lon: ${result.bbox[2].toFixed(6)}°
- Max Lat: ${result.bbox[3].toFixed(6)}°
⏰ Khoảng thời gian: ${result.time_range}
✅ Trạng thái: Hoàn thành
`;
alert(details);
} else {
alert('Chưa có kết quả dự đoán nào!');
}
}
</script>
</body>
</html>