hoàn thành server api dự đoán ra file tiff
This commit is contained in:
+745
-17
@@ -4,6 +4,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Land Classification Training Interface</title>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -19,7 +24,7 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
@@ -46,6 +51,37 @@
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 500px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.map-container {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.map-instructions {
|
||||
background: #e3f2fd;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 4px solid #2196f3;
|
||||
}
|
||||
|
||||
.map-instructions h3 {
|
||||
color: #1976d2;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.map-instructions p {
|
||||
color: #555;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -242,6 +278,49 @@
|
||||
<div class="progress-bar" style="width: 0%;">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Info -->
|
||||
<div style="margin-top: 20px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 10px;">💻 Thông Tin Hệ Thống</h3>
|
||||
<div class="info">
|
||||
<p><strong>🖥️ GPU:</strong> <span id="gpuInfo">Đang tải...</span></p>
|
||||
<p><strong>💾 RAM:</strong> <span id="ramInfo">Đang tải...</span></p>
|
||||
<p><strong>📁 Models:</strong> <span id="modelCount">0</span> models đã train</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Training History -->
|
||||
<div style="margin-top: 20px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 10px;">📜 Lịch Sử Training</h3>
|
||||
<div id="trainingHistory" style="max-height: 300px; overflow-y: auto; background: #f8f9fa; padding: 15px; border-radius: 8px; font-size: 13px;">
|
||||
<p style="color: #999;">Chưa có lịch sử training</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div style="margin-top: 20px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 10px;">📈 Thống Kê Nhanh</h3>
|
||||
<div class="info" style="background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);">
|
||||
<p><strong>🎯 Tổng số lần train:</strong> <span id="totalTrainings">0</span></p>
|
||||
<p><strong>✅ Thành công:</strong> <span id="successTrainings">0</span></p>
|
||||
<p><strong>❌ Thất bại:</strong> <span id="failedTrainings">0</span></p>
|
||||
<p><strong>⏱️ Thời gian TB:</strong> <span id="avgTime">-</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Guide -->
|
||||
<div style="margin-top: 20px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 10px;">📖 Hướng Dẫn Nhanh</h3>
|
||||
<div class="info" style="background: #e7f3ff; border-left: 4px solid #2196F3;">
|
||||
<ol style="margin: 0; padding-left: 20px; font-size: 13px;">
|
||||
<li>Chọn preset hoặc vẽ khu vực trên bản đồ</li>
|
||||
<li>Chọn thời gian và cấu hình vệ tinh</li>
|
||||
<li>Điều chỉnh tham số model (nếu cần)</li>
|
||||
<li>Nhấn "Bắt Đầu Training"</li>
|
||||
<li>Theo dõi tiến độ ở phần này</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Section -->
|
||||
@@ -257,26 +336,27 @@
|
||||
</div>
|
||||
|
||||
<form id="trainingForm">
|
||||
<h3 style="margin-bottom: 15px; color: #667eea;">📍 Khu Vực (Bounding Box)</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Min Longitude:</label>
|
||||
<input type="number" step="0.1" id="minLon" value="105.6" required>
|
||||
<h3 style="margin-bottom: 15px; color: #667eea;">📍 Khu Vực Training</h3>
|
||||
|
||||
<div class="map-container">
|
||||
<div class="map-instructions">
|
||||
<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 #333; vertical-align: middle; margin: 0 5px;">□</span>
|
||||
ở góc trên bên trái của bản đồ để chọn khu vực training
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Min Latitude:</label>
|
||||
<input type="number" step="0.1" id="minLat" value="9.3" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max Longitude:</label>
|
||||
<input type="number" step="0.1" id="maxLon" value="106.2" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max Latitude:</label>
|
||||
<input type="number" step="0.1" id="maxLat" value="9.8" required>
|
||||
<div id="map"></div>
|
||||
<div style="margin-top: 10px; font-size: 13px; color: #666;">
|
||||
<strong>Khu vực đã chọn:</strong>
|
||||
<span id="bboxDisplay">Chưa chọn khu vực</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden inputs to store bbox values -->
|
||||
<input type="hidden" id="minLon" value="105.6" required>
|
||||
<input type="hidden" id="minLat" value="9.3" required>
|
||||
<input type="hidden" id="maxLon" value="106.2" required>
|
||||
<input type="hidden" id="maxLat" value="9.8" required>
|
||||
|
||||
<h3 style="margin: 20px 0 15px; color: #667eea;">📅 Thời Gian</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
@@ -351,18 +431,139 @@
|
||||
<p>Đang tải...</p>
|
||||
</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 style="margin-top: 15px; text-align: center;">
|
||||
<button onclick="downloadPredictionResult()" class="btn btn-secondary" style="background: #28a745;">
|
||||
📥 Tải Kết Quả
|
||||
</button>
|
||||
<button onclick="viewPredictionResult()" class="btn btn-secondary" style="background: #17a2b8; margin-left: 10px;">
|
||||
👁️ Xem Chi Tiết
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:8000/api';
|
||||
let statusInterval = null;
|
||||
let trainingHistory = [];
|
||||
let trainingStats = {
|
||||
total: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
times: []
|
||||
};
|
||||
|
||||
// Load presets on page load
|
||||
window.onload = async () => {
|
||||
await loadPresets();
|
||||
await loadModels();
|
||||
await loadSystemInfo();
|
||||
checkStatus();
|
||||
loadTrainingHistory();
|
||||
};
|
||||
|
||||
// Load preset configurations
|
||||
@@ -397,6 +598,23 @@
|
||||
document.getElementById('maxScenes').value = config.max_scenes;
|
||||
document.getElementById('cloudCover').value = config.cloud_cover;
|
||||
document.getElementById('resolution').value = config.resolution;
|
||||
|
||||
// Update map with new bounds
|
||||
if (currentRectangle) {
|
||||
drawnItems.removeLayer(currentRectangle);
|
||||
}
|
||||
|
||||
const bounds = [[config.min_lat, config.min_lon], [config.max_lat, config.max_lon]];
|
||||
const rectangle = L.rectangle(bounds, {
|
||||
color: '#667eea',
|
||||
weight: 3,
|
||||
fillOpacity: 0.2
|
||||
});
|
||||
|
||||
drawnItems.addLayer(rectangle);
|
||||
currentRectangle = rectangle;
|
||||
map.fitBounds(bounds);
|
||||
updateBboxFromMap(L.latLngBounds(bounds));
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
@@ -494,6 +712,19 @@
|
||||
if (statusInterval) {
|
||||
clearInterval(statusInterval);
|
||||
statusInterval = null;
|
||||
|
||||
// Add to history when training completes
|
||||
if (status.result || status.error) {
|
||||
const record = {
|
||||
timestamp: new Date().toLocaleString('vi-VN'),
|
||||
success: !!status.result,
|
||||
accuracy: status.result?.test_accuracy,
|
||||
error: status.error,
|
||||
duration: status.start_time && status.end_time ?
|
||||
(new Date(status.end_time) - new Date(status.start_time)) / 1000 : null
|
||||
};
|
||||
addTrainingRecord(record);
|
||||
}
|
||||
}
|
||||
|
||||
if (status.result) {
|
||||
@@ -513,14 +744,19 @@
|
||||
const data = await response.json();
|
||||
|
||||
const container = document.getElementById('modelsList');
|
||||
const modelSelect = document.getElementById('selectedModel');
|
||||
|
||||
if (data.models.length === 0) {
|
||||
container.innerHTML = '<p>Chưa có model nào</p>';
|
||||
modelSelect.innerHTML = '<option value="">-- Chưa có model --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
modelSelect.innerHTML = '<option value="">-- Chọn model --</option>';
|
||||
|
||||
data.models.forEach(model => {
|
||||
// Add to models list display
|
||||
const item = document.createElement('div');
|
||||
item.className = 'model-item';
|
||||
item.innerHTML = `
|
||||
@@ -531,13 +767,505 @@
|
||||
${model.info.test_accuracy ? `<p><strong>Test Accuracy:</strong> ${(model.info.test_accuracy * 100).toFixed(2)}%</p>` : ''}
|
||||
`;
|
||||
container.appendChild(item);
|
||||
|
||||
// Add to model selection dropdown
|
||||
const option = document.createElement('option');
|
||||
option.value = model.filename;
|
||||
option.textContent = `${model.filename} (${model.size_mb} MB)`;
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Update model count
|
||||
document.getElementById('modelCount').textContent = data.models.length;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading models:', error);
|
||||
document.getElementById('modelsList').innerHTML = '<p>Lỗi khi tải danh sách models</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load system information
|
||||
async function loadSystemInfo() {
|
||||
try {
|
||||
// Get GPU info
|
||||
const gpuInfo = 'RTX 4060 (Available)';
|
||||
document.getElementById('gpuInfo').textContent = gpuInfo;
|
||||
|
||||
// Get RAM info (mock data - in real app would come from API)
|
||||
const ramInfo = '32 GB (16 GB available)';
|
||||
document.getElementById('ramInfo').textContent = ramInfo;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading system info:', error);
|
||||
document.getElementById('gpuInfo').textContent = 'N/A';
|
||||
document.getElementById('ramInfo').textContent = 'N/A';
|
||||
}
|
||||
}
|
||||
|
||||
// Load training history from localStorage
|
||||
function loadTrainingHistory() {
|
||||
const saved = localStorage.getItem('trainingHistory');
|
||||
if (saved) {
|
||||
trainingHistory = JSON.parse(saved);
|
||||
updateTrainingHistoryDisplay();
|
||||
updateTrainingStats();
|
||||
}
|
||||
}
|
||||
|
||||
// Save training history to localStorage
|
||||
function saveTrainingHistory() {
|
||||
localStorage.setItem('trainingHistory', JSON.stringify(trainingHistory));
|
||||
}
|
||||
|
||||
// Add training record to history
|
||||
function addTrainingRecord(record) {
|
||||
trainingHistory.unshift(record);
|
||||
if (trainingHistory.length > 20) {
|
||||
trainingHistory = trainingHistory.slice(0, 20);
|
||||
}
|
||||
saveTrainingHistory();
|
||||
updateTrainingHistoryDisplay();
|
||||
updateTrainingStats();
|
||||
}
|
||||
|
||||
// Update training history display
|
||||
function updateTrainingHistoryDisplay() {
|
||||
const container = document.getElementById('trainingHistory');
|
||||
|
||||
if (trainingHistory.length === 0) {
|
||||
container.innerHTML = '<p style="color: #999;">Chưa có lịch sử training</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = trainingHistory.map(record => {
|
||||
const statusIcon = record.success ? '✅' : '❌';
|
||||
const statusColor = record.success ? '#4caf50' : '#f44336';
|
||||
const duration = record.duration ? ` (${Math.round(record.duration / 60)}m)` : '';
|
||||
|
||||
return `
|
||||
<div style="padding: 8px; margin-bottom: 8px; background: white; border-radius: 4px; border-left: 3px solid ${statusColor};">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-weight: 600;">${statusIcon} ${record.timestamp}</span>
|
||||
<span style="font-size: 11px; color: #666;">${duration}</span>
|
||||
</div>
|
||||
${record.accuracy ? `<div style="font-size: 12px; color: #666; margin-top: 4px;">Accuracy: ${(record.accuracy * 100).toFixed(1)}%</div>` : ''}
|
||||
${record.error ? `<div style="font-size: 11px; color: #f44336; margin-top: 4px;">${record.error}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Update training statistics
|
||||
function updateTrainingStats() {
|
||||
const total = trainingHistory.length;
|
||||
const success = trainingHistory.filter(r => r.success).length;
|
||||
const failed = total - success;
|
||||
|
||||
document.getElementById('totalTrainings').textContent = total;
|
||||
document.getElementById('successTrainings').textContent = success;
|
||||
document.getElementById('failedTrainings').textContent = failed;
|
||||
|
||||
// Calculate average time
|
||||
const times = trainingHistory.filter(r => r.duration).map(r => r.duration);
|
||||
if (times.length > 0) {
|
||||
const avgSeconds = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
const avgMinutes = Math.round(avgSeconds / 60);
|
||||
document.getElementById('avgTime').textContent = `${avgMinutes} phút`;
|
||||
} else {
|
||||
document.getElementById('avgTime').textContent = '-';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Leaflet Map
|
||||
let map, drawnItems, currentRectangle;
|
||||
|
||||
function initMap() {
|
||||
// Initialize map centered on Vietnam Mekong Delta
|
||||
map = L.map('map').setView([9.55, 105.9], 9);
|
||||
|
||||
// Add OpenStreetMap tile layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 18
|
||||
}).addTo(map);
|
||||
|
||||
// Initialize feature group for drawn items
|
||||
drawnItems = new L.FeatureGroup();
|
||||
map.addLayer(drawnItems);
|
||||
|
||||
// Initialize draw control with only rectangle tool
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polyline: false,
|
||||
polygon: false,
|
||||
circle: false,
|
||||
marker: false,
|
||||
circlemarker: false,
|
||||
rectangle: {
|
||||
shapeOptions: {
|
||||
color: '#667eea',
|
||||
weight: 3,
|
||||
fillOpacity: 0.2
|
||||
}
|
||||
}
|
||||
},
|
||||
edit: {
|
||||
featureGroup: drawnItems,
|
||||
remove: true
|
||||
}
|
||||
});
|
||||
map.addControl(drawControl);
|
||||
|
||||
// Handle rectangle creation
|
||||
map.on(L.Draw.Event.CREATED, function(event) {
|
||||
const layer = event.layer;
|
||||
|
||||
// Remove previous rectangle if exists
|
||||
if (currentRectangle) {
|
||||
drawnItems.removeLayer(currentRectangle);
|
||||
}
|
||||
|
||||
// Add new rectangle
|
||||
drawnItems.addLayer(layer);
|
||||
currentRectangle = layer;
|
||||
|
||||
// Get bounds and update form
|
||||
const bounds = layer.getBounds();
|
||||
updateBboxFromMap(bounds);
|
||||
});
|
||||
|
||||
// Handle rectangle edit
|
||||
map.on(L.Draw.Event.EDITED, function(event) {
|
||||
const layers = event.layers;
|
||||
layers.eachLayer(function(layer) {
|
||||
const bounds = layer.getBounds();
|
||||
updateBboxFromMap(bounds);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle rectangle deletion
|
||||
map.on(L.Draw.Event.DELETED, function() {
|
||||
currentRectangle = null;
|
||||
document.getElementById('bboxDisplay').textContent = 'Chưa chọn khu vực';
|
||||
// Reset to default values
|
||||
document.getElementById('minLon').value = '';
|
||||
document.getElementById('minLat').value = '';
|
||||
document.getElementById('maxLon').value = '';
|
||||
document.getElementById('maxLat').value = '';
|
||||
});
|
||||
|
||||
// Draw initial rectangle based on default values
|
||||
drawInitialRectangle();
|
||||
}
|
||||
|
||||
function updateBboxFromMap(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('minLat').value = south;
|
||||
document.getElementById('minLon').value = west;
|
||||
document.getElementById('maxLat').value = north;
|
||||
document.getElementById('maxLon').value = east;
|
||||
|
||||
document.getElementById('bboxDisplay').textContent =
|
||||
`Lon: ${west} → ${east}, Lat: ${south} → ${north}`;
|
||||
}
|
||||
|
||||
function drawInitialRectangle() {
|
||||
const minLon = parseFloat(document.getElementById('minLon').value);
|
||||
const minLat = parseFloat(document.getElementById('minLat').value);
|
||||
const maxLon = parseFloat(document.getElementById('maxLon').value);
|
||||
const maxLat = parseFloat(document.getElementById('maxLat').value);
|
||||
|
||||
if (minLon && minLat && maxLon && maxLat) {
|
||||
const bounds = [[minLat, minLon], [maxLat, maxLon]];
|
||||
const rectangle = L.rectangle(bounds, {
|
||||
color: '#667eea',
|
||||
weight: 3,
|
||||
fillOpacity: 0.2
|
||||
});
|
||||
|
||||
drawnItems.addLayer(rectangle);
|
||||
currentRectangle = rectangle;
|
||||
map.fitBounds(bounds);
|
||||
updateBboxFromMap(L.latLngBounds(bounds));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize map when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initMap();
|
||||
initPredictionMap();
|
||||
});
|
||||
|
||||
// ============== 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;
|
||||
|
||||
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>
|
||||
`;
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
// Download prediction result
|
||||
function downloadPredictionResult() {
|
||||
if (window.lastPredictionResult) {
|
||||
const result = window.lastPredictionResult;
|
||||
alert('File kết quả: ' + result.output_file + '\n\nĐể tải file, vui lòng truy cập thư mục predictions/ trên server.');
|
||||
} else {
|
||||
alert('Chưa có kết quả dự đoán nào!');
|
||||
}
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
||||
Reference in New Issue
Block a user