790 lines
35 KiB
HTML
790 lines
35 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="vi">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Batch Processing - Land Classification</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;
|
||
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: 1600px;
|
||
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;
|
||
}
|
||
|
||
.content {
|
||
padding: 30px;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 30px;
|
||
}
|
||
|
||
.section {
|
||
background: #f8f9fa;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.section h2 {
|
||
color: #667eea;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
color: #333;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.form-group input, .form-group select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 5px;
|
||
font-size: 1em;
|
||
}
|
||
|
||
.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-success {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.batch-item {
|
||
background: white;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
border-radius: 8px;
|
||
border-left: 4px solid #667eea;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 20px;
|
||
}
|
||
|
||
.batch-item.completed {
|
||
border-left-color: #28a745;
|
||
}
|
||
|
||
.batch-item.failed {
|
||
border-left-color: #dc3545;
|
||
}
|
||
|
||
.batch-item.running {
|
||
border-left-color: #ffc107;
|
||
}
|
||
|
||
.progress {
|
||
height: 25px;
|
||
background: #e0e0e0;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||
transition: width 0.3s;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-weight: 600;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.alert {
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.alert-info {
|
||
background: #d1ecf1;
|
||
border-left: 4px solid #0c5460;
|
||
color: #0c5460;
|
||
}
|
||
|
||
.alert-success {
|
||
background: #d4edda;
|
||
border-left: 4px solid #155724;
|
||
color: #155724;
|
||
}
|
||
|
||
.jobs-list {
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
#batchMap {
|
||
height: 400px;
|
||
border-radius: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🚀 Batch Processing</h1>
|
||
<p>Xử lý nhiều khu vực cùng lúc với model đã train</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: #f093fb; 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 (Active)</a>
|
||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
|
||
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<!-- Configuration Section -->
|
||
<div class="section">
|
||
<h2>⚙️ Cấu hình Batch</h2>
|
||
|
||
<div class="form-group">
|
||
<label for="batchModelSelect">Model:</label>
|
||
<select id="batchModelSelect">
|
||
<option value="">Đang tải...</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="batchName">Tên khu vực:</label>
|
||
<input type="text" id="batchName" placeholder="Ví dụ: Khu vực A">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Bbox (từ bản đồ hoặc nhập thủ công):</label>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||
<input type="number" id="batchMinLon" placeholder="Min Lon" step="0.0001">
|
||
<input type="number" id="batchMinLat" placeholder="Min Lat" step="0.0001">
|
||
<input type="number" id="batchMaxLon" placeholder="Max Lon" step="0.0001">
|
||
<input type="number" id="batchMaxLat" placeholder="Max Lat" step="0.0001">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Thời gian:</label>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||
<input type="date" id="batchStartDate" value="2023-03-01">
|
||
<input type="date" id="batchEndDate" value="2023-05-31">
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-primary" onclick="addBatchItem()">
|
||
➕ Thêm vào Batch
|
||
</button>
|
||
|
||
<!-- Map for selecting bbox -->
|
||
<div id="batchMap"></div>
|
||
</div>
|
||
|
||
<!-- Batch Queue Section -->
|
||
<div class="section">
|
||
<h2>📋 Batch Queue (<span id="queueCount">0</span> items)</h2>
|
||
|
||
<div id="batchQueue" class="jobs-list">
|
||
<p style="text-align: center; color: #666;">Chưa có item nào. Thêm khu vực từ bên trái.</p>
|
||
</div>
|
||
|
||
<div style="margin-top: 20px;">
|
||
<button class="btn btn-success" onclick="startBatch()" id="startBatchBtn" disabled>
|
||
🚀 Start Batch Processing
|
||
</button>
|
||
<button class="btn btn-danger" onclick="clearBatchQueue()">
|
||
🗑️ Clear Queue
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status Section -->
|
||
<div class="section" style="grid-column: 1 / -1;">
|
||
<h2>📊 Batch Status</h2>
|
||
|
||
<div id="batchStatus" style="display: none;">
|
||
<div class="alert alert-info">
|
||
<p><strong>Batch ID:</strong> <span id="currentBatchId"></span></p>
|
||
<p><strong>Status:</strong> Queued: <span id="statusQueued">0</span> | Running: <span id="statusRunning">0</span> | Completed: <span id="statusCompleted">0</span> | Failed: <span id="statusFailed">0</span></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="runningJobs" class="jobs-list">
|
||
<!-- Running jobs will appear here -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Results Section -->
|
||
<div class="section" style="grid-column: 1 / -1;">
|
||
<h2>✅ Completed Results</h2>
|
||
|
||
<div style="margin-bottom: 15px; display: flex; gap: 10px; align-items: center;">
|
||
<button class="btn btn-primary" onclick="loadAllBatchResults()" style="padding: 8px 20px;">
|
||
🔄 Refresh Results
|
||
</button>
|
||
<button class="btn btn-success" onclick="downloadAllResults()" style="padding: 8px 20px;">
|
||
📦 Download All (Bulk)
|
||
</button>
|
||
</div>
|
||
|
||
<div id="completedResults" class="jobs-list">
|
||
<p style="text-align: center; color: #666;">Chưa có kết quả nào</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal for large preview -->
|
||
<div id="previewModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 10000; padding: 20px;">
|
||
<div style="position: relative; height: 100%; display: flex; align-items: center; justify-content: center;">
|
||
<button onclick="closePreviewModal()" style="position: absolute; top: 20px; right: 20px; background: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 18px; font-weight: bold;">
|
||
✕ Close
|
||
</button>
|
||
<img id="previewImage" style="max-width: 90%; max-height: 90%; border-radius: 10px;">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Scripts -->
|
||
<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>
|
||
let map, drawnItems, drawControl;
|
||
let batchQueue = [];
|
||
let currentBatchId = null;
|
||
let statusCheckInterval = null;
|
||
|
||
// Initialize map
|
||
function initMap() {
|
||
map = L.map('batchMap').setView([9.5, 105.9], 9);
|
||
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap contributors'
|
||
}).addTo(map);
|
||
|
||
drawnItems = new L.FeatureGroup();
|
||
map.addLayer(drawnItems);
|
||
|
||
drawControl = new L.Control.Draw({
|
||
draw: {
|
||
rectangle: true,
|
||
polygon: false,
|
||
circle: false,
|
||
marker: false,
|
||
polyline: false,
|
||
circlemarker: false
|
||
},
|
||
edit: {
|
||
featureGroup: drawnItems,
|
||
remove: true
|
||
}
|
||
});
|
||
map.addControl(drawControl);
|
||
|
||
map.on(L.Draw.Event.CREATED, function(event) {
|
||
drawnItems.clearLayers();
|
||
const layer = event.layer;
|
||
drawnItems.addLayer(layer);
|
||
|
||
const bounds = layer.getBounds();
|
||
document.getElementById('batchMinLon').value = bounds.getWest().toFixed(4);
|
||
document.getElementById('batchMinLat').value = bounds.getSouth().toFixed(4);
|
||
document.getElementById('batchMaxLon').value = bounds.getEast().toFixed(4);
|
||
document.getElementById('batchMaxLat').value = bounds.getNorth().toFixed(4);
|
||
});
|
||
}
|
||
|
||
// Load models
|
||
async function loadModels() {
|
||
try {
|
||
const response = await fetch('/api/models/list');
|
||
const data = await response.json();
|
||
|
||
const select = document.getElementById('batchModelSelect');
|
||
select.innerHTML = '<option value="">Chọn model...</option>';
|
||
|
||
data.models.filter(m => m.filename.endsWith('.joblib')).forEach(model => {
|
||
const option = document.createElement('option');
|
||
option.value = model.filename;
|
||
option.textContent = `${model.filename} - ${model.created}`;
|
||
select.appendChild(option);
|
||
});
|
||
|
||
if (data.models.length > 0) {
|
||
select.value = data.models[0].filename;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading models:', error);
|
||
}
|
||
}
|
||
|
||
// Add item to batch queue
|
||
function addBatchItem() {
|
||
const name = document.getElementById('batchName').value;
|
||
const minLon = parseFloat(document.getElementById('batchMinLon').value);
|
||
const minLat = parseFloat(document.getElementById('batchMinLat').value);
|
||
const maxLon = parseFloat(document.getElementById('batchMaxLon').value);
|
||
const maxLat = parseFloat(document.getElementById('batchMaxLat').value);
|
||
const startDate = document.getElementById('batchStartDate').value;
|
||
const endDate = document.getElementById('batchEndDate').value;
|
||
|
||
if (!name || isNaN(minLon) || isNaN(minLat) || isNaN(maxLon) || isNaN(maxLat)) {
|
||
alert('❌ Vui lòng điền đầy đủ thông tin!');
|
||
return;
|
||
}
|
||
|
||
const item = {
|
||
name,
|
||
min_lon: minLon,
|
||
min_lat: minLat,
|
||
max_lon: maxLon,
|
||
max_lat: maxLat,
|
||
start_date: startDate,
|
||
end_date: endDate,
|
||
max_scenes: 12,
|
||
cloud_cover: 30,
|
||
resolution: 20
|
||
};
|
||
|
||
batchQueue.push(item);
|
||
updateBatchQueueDisplay();
|
||
|
||
// Clear form
|
||
document.getElementById('batchName').value = '';
|
||
drawnItems.clearLayers();
|
||
}
|
||
|
||
// Update batch queue display
|
||
function updateBatchQueueDisplay() {
|
||
const queueDiv = document.getElementById('batchQueue');
|
||
const countSpan = document.getElementById('queueCount');
|
||
|
||
countSpan.textContent = batchQueue.length;
|
||
|
||
if (batchQueue.length === 0) {
|
||
queueDiv.innerHTML = '<p style="text-align: center; color: #666;">Chưa có item nào. Thêm khu vực từ bên trái.</p>';
|
||
document.getElementById('startBatchBtn').disabled = true;
|
||
return;
|
||
}
|
||
|
||
document.getElementById('startBatchBtn').disabled = false;
|
||
|
||
queueDiv.innerHTML = batchQueue.map((item, idx) => `
|
||
<div class="batch-item">
|
||
<div>
|
||
<strong>${item.name}</strong><br>
|
||
<small>Bbox: (${item.min_lon.toFixed(2)}, ${item.min_lat.toFixed(2)}) → (${item.max_lon.toFixed(2)}, ${item.max_lat.toFixed(2)})</small><br>
|
||
<small>Time: ${item.start_date} → ${item.end_date}</small>
|
||
</div>
|
||
<button class="btn btn-danger" style="padding: 5px 15px;" onclick="removeBatchItem(${idx})">
|
||
❌
|
||
</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// Remove item from queue
|
||
function removeBatchItem(index) {
|
||
batchQueue.splice(index, 1);
|
||
updateBatchQueueDisplay();
|
||
}
|
||
|
||
// Clear batch queue
|
||
function clearBatchQueue() {
|
||
if (!confirm('Xóa tất cả items trong queue?')) return;
|
||
batchQueue = [];
|
||
updateBatchQueueDisplay();
|
||
}
|
||
|
||
// Start batch processing
|
||
async function startBatch() {
|
||
const modelFilename = document.getElementById('batchModelSelect').value;
|
||
if (!modelFilename) {
|
||
alert('❌ Vui lòng chọn model!');
|
||
return;
|
||
}
|
||
|
||
if (batchQueue.length === 0) {
|
||
alert('❌ Batch queue trống!');
|
||
return;
|
||
}
|
||
|
||
const config = {
|
||
model_filename: modelFilename,
|
||
items: batchQueue,
|
||
auto_retry: true,
|
||
max_retries: 3
|
||
};
|
||
|
||
try {
|
||
const response = await fetch('/api/batch/start', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(config)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
currentBatchId = result.batch_id;
|
||
document.getElementById('currentBatchId').textContent = currentBatchId;
|
||
document.getElementById('batchStatus').style.display = 'block';
|
||
|
||
// Clear local queue
|
||
batchQueue = [];
|
||
updateBatchQueueDisplay();
|
||
|
||
// Start monitoring
|
||
startStatusCheck();
|
||
|
||
alert(`✅ Đã bắt đầu batch processing với ${result.total_jobs} jobs!`);
|
||
} else {
|
||
throw new Error(result.detail || 'Lỗi khi bắt đầu batch');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error starting batch:', error);
|
||
alert('❌ Lỗi: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Check batch status
|
||
async function checkBatchStatus() {
|
||
try {
|
||
const response = await fetch('/api/batch/status');
|
||
const status = await response.json();
|
||
|
||
// Update status counts
|
||
document.getElementById('statusQueued').textContent = status.queue.queued;
|
||
document.getElementById('statusRunning').textContent = status.queue.running;
|
||
document.getElementById('statusCompleted').textContent = status.queue.completed;
|
||
document.getElementById('statusFailed').textContent = status.queue.failed;
|
||
|
||
// Update running jobs
|
||
const runningDiv = document.getElementById('runningJobs');
|
||
if (status.jobs.running.length > 0) {
|
||
runningDiv.innerHTML = status.jobs.running.map(job => {
|
||
const outputFile = job.result?.output_file || '';
|
||
const pngFile = job.result?.png_file || '';
|
||
const outputFilename = outputFile ? outputFile.split('/').pop() : '';
|
||
const pngFilename = pngFile ? pngFile.split('/').pop() : '';
|
||
return `
|
||
<div class="batch-item running">
|
||
<div style="flex: 1;">
|
||
<strong>${job.name}</strong> - <span style="color: #ffc107;">Running</span><br>
|
||
<small>Job ID: ${job.job_id}</small>
|
||
<div class="progress">
|
||
<div class="progress-bar" style="width: ${job.progress}%">${job.progress}%</div>
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 200px; flex-shrink: 0;">
|
||
<button class="btn btn-success" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||
onclick="downloadResult('${outputFilename}')"
|
||
${outputFilename ? '' : 'disabled'}>
|
||
💾 Download GeoTIFF
|
||
</button>
|
||
<button class="btn btn-primary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||
onclick="downloadPNG('${pngFilename}')"
|
||
${pngFilename ? '' : 'disabled'}>
|
||
🖼️ Download PNG
|
||
</button>
|
||
<button class="btn btn-secondary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||
onclick="viewLargePNG('${pngFilename}')"
|
||
${pngFilename ? '' : 'disabled'}>
|
||
🔍 View Preview
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
} else {
|
||
runningDiv.innerHTML = '<p style="text-align: center; color: #666;">Không có job nào đang chạy</p>';
|
||
}
|
||
|
||
// Update completed results
|
||
const completedDiv = document.getElementById('completedResults');
|
||
if (status.jobs.recent_completed.length > 0) {
|
||
completedDiv.innerHTML = status.jobs.recent_completed.map(job => {
|
||
const outputFile = job.result?.output_file || '';
|
||
const pngFile = job.result?.png_file || '';
|
||
const outputFilename = outputFile ? outputFile.split('/').pop() : '';
|
||
const pngFilename = pngFile ? pngFile.split('/').pop() : '';
|
||
return `
|
||
<div class="batch-item completed">
|
||
<div style="flex: 1;">
|
||
<strong>${job.name}</strong> - <span style="color: #28a745;">✓ Completed</span><br>
|
||
<small>Job ID: ${job.job_id}</small><br>
|
||
<small>Completed: ${new Date(job.completed_at).toLocaleString()}</small><br>
|
||
<small><strong>Shape:</strong> ${job.result?.shape ? job.result.shape.join(' x ') : 'N/A'}</small><br>
|
||
<small><strong>Classes:</strong> ${job.result?.unique_classes ? job.result.unique_classes.join(', ') : 'N/A'}</small><br>
|
||
<small><strong>Features:</strong> ${job.result?.n_features || 'N/A'}</small><br>
|
||
<small><strong>Output:</strong> ${outputFilename || 'N/A'}</small><br>
|
||
<div style="margin-top: 10px;">
|
||
<img src="/api/predictions/preview/${pngFilename}"
|
||
style="max-width: 100%; max-height: 300px; border-radius: 5px; cursor: pointer; ${pngFilename ? '' : 'display:none;'}"
|
||
onclick="viewLargePNG('${pngFilename}')"
|
||
title="Click để xem lớn hơn"
|
||
onerror="this.style.display='none'">
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 200px; flex-shrink: 0;">
|
||
<button class="btn btn-success" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||
onclick="downloadResult('${outputFilename}')"
|
||
${outputFilename ? '' : 'disabled'}>
|
||
💾 Download GeoTIFF
|
||
</button>
|
||
<button class="btn btn-primary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||
onclick="downloadPNG('${pngFilename}')"
|
||
${pngFilename ? '' : 'disabled'}>
|
||
🖼️ Download PNG
|
||
</button>
|
||
<button class="btn btn-secondary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||
onclick="viewLargePNG('${pngFilename}')"
|
||
${pngFilename ? '' : 'disabled'}>
|
||
🔍 View Preview
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
} else {
|
||
completedDiv.innerHTML = '<p style="text-align: center; color: #666;">Chưa có kết quả nào</p>';
|
||
}
|
||
|
||
// Show failed jobs if any
|
||
if (status.jobs.recent_failed.length > 0) {
|
||
const failedHTML = status.jobs.recent_failed.map(job => {
|
||
const outputFile = job.result?.output_file || '';
|
||
const pngFile = job.result?.png_file || '';
|
||
const outputFilename = outputFile ? outputFile.split('/').pop() : '';
|
||
const pngFilename = pngFile ? pngFile.split('/').pop() : '';
|
||
return `
|
||
<div class="batch-item failed">
|
||
<div style="flex: 1;">
|
||
<strong>${job.name}</strong> - <span style="color: #dc3545;">✗ Failed</span><br>
|
||
<small>Job ID: ${job.job_id}</small><br>
|
||
<small style="color: #dc3545;">${job.error || 'Unknown error'}</small>
|
||
</div>
|
||
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 200px; flex-shrink: 0;">
|
||
<button class="btn btn-success" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||
onclick="downloadResult('${outputFilename}')"
|
||
${outputFilename ? '' : 'disabled'}>
|
||
💾 Download GeoTIFF
|
||
</button>
|
||
<button class="btn btn-primary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||
onclick="downloadPNG('${pngFilename}')"
|
||
${pngFilename ? '' : 'disabled'}>
|
||
🖼️ Download PNG
|
||
</button>
|
||
<button class="btn btn-secondary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||
onclick="viewLargePNG('${pngFilename}')"
|
||
${pngFilename ? '' : 'disabled'}>
|
||
🔍 View Preview
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
completedDiv.innerHTML += '<h3 style="margin-top: 20px; color: #dc3545;">❌ Failed Jobs</h3>' + failedHTML;
|
||
}
|
||
|
||
// Stop checking if all done
|
||
if (status.queue.running === 0 && status.queue.queued === 0 && currentBatchId) {
|
||
stopStatusCheck();
|
||
alert('✅ Batch processing hoàn thành!');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error checking batch status:', error);
|
||
}
|
||
}
|
||
|
||
// Download result
|
||
function downloadResult(filename) {
|
||
window.location.href = `/api/predictions/download/${filename}`;
|
||
}
|
||
|
||
// Download PNG
|
||
function downloadPNG(filename) {
|
||
window.location.href = `/api/predictions/preview/${filename}`;
|
||
}
|
||
|
||
// View large PNG in new window
|
||
function viewLargePNG(filename) {
|
||
const modal = document.getElementById('previewModal');
|
||
const img = document.getElementById('previewImage');
|
||
img.src = `/api/predictions/preview/${filename}`;
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
// Close preview modal
|
||
function closePreviewModal() {
|
||
document.getElementById('previewModal').style.display = 'none';
|
||
}
|
||
|
||
// Load all batch results
|
||
async function loadAllBatchResults() {
|
||
try {
|
||
const response = await fetch('/api/batch/status');
|
||
const status = await response.json();
|
||
|
||
const completedDiv = document.getElementById('completedResults');
|
||
|
||
// Combine recent_completed from status
|
||
const allCompleted = status.jobs.recent_completed || [];
|
||
|
||
if (allCompleted.length === 0) {
|
||
completedDiv.innerHTML = '<p style="text-align: center; color: #666;">Chưa có kết quả nào</p>';
|
||
return;
|
||
}
|
||
|
||
completedDiv.innerHTML = allCompleted.map(job => `
|
||
<div class="batch-item completed">
|
||
<div style="flex: 1;">
|
||
<strong>${job.name}</strong> - <span style="color: #28a745;">✓ Completed</span><br>
|
||
<small>Job ID: ${job.job_id}</small><br>
|
||
<small>Completed: ${new Date(job.completed_at).toLocaleString()}</small><br>
|
||
${job.result ? `
|
||
<small><strong>Shape:</strong> ${job.result.shape.join(' x ')}</small><br>
|
||
<small><strong>Classes:</strong> ${job.result.unique_classes.join(', ')}</small><br>
|
||
<small><strong>Features:</strong> ${job.result.n_features}</small><br>
|
||
<small><strong>Model:</strong> ${job.result.model_used}</small><br>
|
||
${job.result.png_file ? `
|
||
<div style="margin-top: 10px;">
|
||
<img src="/api/predictions/preview/${job.result.png_file.split('/').pop()}"
|
||
style="max-width: 100%; border-radius: 5px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2);"
|
||
onclick="viewLargePNG('${job.result.png_file.split('/').pop()}')"
|
||
title="Click để xem lớn hơn">
|
||
</div>
|
||
` : ''}
|
||
` : ''}
|
||
</div>
|
||
<div style="display: flex; flex-direction: column; gap: 5px; min-width: 200px;">
|
||
${job.result && job.result.output_file ? `
|
||
<button class="btn btn-success" style="padding: 8px 20px;" onclick="downloadResult('${job.result.output_file.split('/').pop()}')">
|
||
💾 Download GeoTIFF
|
||
</button>
|
||
${job.result.png_file ? `
|
||
<button class="btn btn-primary" style="padding: 8px 20px;" onclick="downloadPNG('${job.result.png_file.split('/').pop()}')">
|
||
🖼️ Download PNG
|
||
</button>
|
||
<button class="btn btn-secondary" style="padding: 8px 20px;" onclick="viewLargePNG('${job.result.png_file.split('/').pop()}')">
|
||
🔍 View Preview
|
||
</button>
|
||
` : ''}
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
} catch (error) {
|
||
console.error('Error loading batch results:', error);
|
||
}
|
||
}
|
||
|
||
// Download all results as ZIP (placeholder)
|
||
function downloadAllResults() {
|
||
alert('💡 Tính năng download tất cả batch results sẽ được thêm trong phiên bản tiếp theo.\\nHiện tại vui lòng download từng file riêng lẻ.');
|
||
}
|
||
|
||
// Start/stop status monitoring
|
||
function startStatusCheck() {
|
||
if (statusCheckInterval) clearInterval(statusCheckInterval);
|
||
statusCheckInterval = setInterval(checkBatchStatus, 3000);
|
||
}
|
||
|
||
function stopStatusCheck() {
|
||
if (statusCheckInterval) {
|
||
clearInterval(statusCheckInterval);
|
||
statusCheckInterval = null;
|
||
}
|
||
}
|
||
|
||
// Initialize on load
|
||
window.onload = function() {
|
||
initMap();
|
||
loadModels();
|
||
};
|
||
|
||
// Cleanup on unload
|
||
window.onbeforeunload = function() {
|
||
stopStatusCheck();
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|