update chức năng ndvi time seriese
This commit is contained in:
@@ -0,0 +1,788 @@
|
||||
<!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>
|
||||
</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>
|
||||
Reference in New Issue
Block a user