Files
remote-sensing/change_detection_interface.html
T

384 lines
20 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Change Detection - Compare Current vs Future Land Use</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.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%); min-height: 100vh; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; background: white; border-radius: 12px; 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: 32px; margin-bottom: 10px; }
.header p { font-size: 16px; opacity: 0.9; }
.content { padding: 30px; display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
.left-panel, .right-panel { display: flex; flex-direction: column; gap: 20px; }
#map { width: 100%; height: 400px; border-radius: 8px; border: 2px solid #e0e0e0; }
.section { background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #667eea; }
.section h2 { color: #333; font-size: 18px; margin-bottom: 15px; display: flex; align-items: center; gap: 8px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 6px; color: #555; font-weight: 500; font-size: 14px; }
.form-group input[type="text"], .form-group input[type="date"], .form-group input[type="number"], .form-group select { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; font-family: inherit; transition: all 0.3s ease; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.bbox-display { background: white; padding: 12px; border-radius: 6px; font-size: 13px; color: #666; font-family: monospace; border: 1px dashed #667eea; word-break: break-all; }
.btn { padding: 12px 24px; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; }
.btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.result { background: white; border: 2px solid #e0e0e0; border-radius: 8px; padding: 20px; display: none; animation: slideIn 0.3s ease; max-height: 600px; overflow-y: auto; }
.result.success { border-color: #4caf50; background: #f1f8f5; }
.result.error { border-color: #f44336; background: #fdf5f4; }
.result.processing { border-color: #2196f3; background: #f3f8fd; }
.result h3 { margin-bottom: 15px; color: #333; }
.result table { width: 100%; border-collapse: collapse; margin: 15px 0; }
.result table th, .result table td { padding: 10px; text-align: left; border-bottom: 1px solid #e0e0e0; }
.result table th { background: #f0f0f0; font-weight: 600; color: #333; }
.result pre { background: #f5f5f5; padding: 15px; border-radius: 6px; overflow-x: auto; font-size: 12px; color: #333; max-height: 300px; overflow-y: auto; border-left: 4px solid #667eea; }
.error-text { color: #f44336; font-weight: 500; }
.success-text { color: #4caf50; font-weight: 500; }
.processing-text { color: #2196f3; font-weight: 500; }
.progress { width: 100%; height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden; margin: 10px 0; }
.progress-bar { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); width: 0%; transition: width 0.3s ease; }
.stat-box { background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #667eea; margin: 10px 0; }
.stat-label { font-size: 12px; color: #999; text-transform: uppercase; margin-bottom: 5px; }
.stat-value { font-size: 20px; font-weight: 600; color: #333; }
.info-box { background: #e3f2fd; padding: 12px; border-radius: 6px; border-left: 4px solid #2196f3; font-size: 13px; color: #1565c0; }
@keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 1024px) { .content { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔍 Change Detection - Land Use Analysis</h1>
<p>Compare current land use with predicted future changes</p>
</div>
<div class="content">
<!-- Left Panel -->
<div class="left-panel">
<div class="section">
<h2><span>🗺️</span>Select Area on Map</h2>
<p style="color: #999; font-size: 13px; margin-bottom: 10px;">Click on map to select bounding box</p>
<div id="map"></div>
<div class="form-group" style="margin-top: 10px;">
<label>BBox (min_lon, min_lat, max_lon, max_lat)</label>
<div class="bbox-display" id="bboxDisplay">Click on map to select area</div>
</div>
</div>
<div class="section">
<h2><span>📅</span>Current Period (Baseline)</h2>
<div class="form-row">
<div class="form-group">
<label>Start Date</label>
<input type="date" id="currentStartDate" value="2022-01-01">
</div>
<div class="form-group">
<label>End Date</label>
<input type="date" id="currentEndDate" value="2022-03-31">
</div>
</div>
</div>
<div class="section">
<h2><span>🔮</span>Prediction Period (Future)</h2>
<div class="form-row">
<div class="form-group">
<label>Start Date</label>
<input type="date" id="predictionStartDate" value="2023-01-01">
</div>
<div class="form-group">
<label>End Date</label>
<input type="date" id="predictionEndDate" value="2023-03-31">
</div>
</div>
</div>
<div class="section">
<h2><span>⚙️</span>Parameters</h2>
<div class="form-row">
<div class="form-group">
<label>Max Scenes</label>
<input type="number" id="maxScenes" value="12" min="1" max="100">
</div>
<div class="form-group">
<label>Cloud Cover %</label>
<input type="number" id="cloudCover" value="30" min="0" max="100">
</div>
</div>
<div class="form-group">
<label>Resolution (m)</label>
<input type="number" id="resolution" value="20" min="10" max="100" step="10">
</div>
</div>
</div>
<!-- Right Panel -->
<div class="right-panel">
<div class="section">
<h2><span>🤖</span>Select Trained Model</h2>
<div class="form-group">
<label>Trained Model</label>
<select id="modelSelect">
<option value="">Loading models...</option>
</select>
</div>
<div id="modelInfo" style="font-size: 12px; color: #999; margin-top: 10px;"></div>
</div>
<div class="section">
<h2><span></span>Workflow</h2>
<div class="info-box">
1️⃣ Classify current period satellite data<br>
2️⃣ Classify future period satellite data<br>
3️⃣ Compare to detect land use changes
</div>
</div>
<div class="section">
<button class="btn btn-primary" id="runBtn" onclick="runChangeDetection()" disabled>
<span>▶️</span>Compare Periods
</button>
</div>
<div id="resultDiv" class="result"></div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const API_BASE = 'http://localhost:8000/api';
let map, rectangle;
let bbox = null;
function initMap() {
map = L.map('map').setView([9.8, 105.85], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
const defaultBbox = [105.6, 9.3, 106.2, 9.8];
drawBboxRectangle(defaultBbox);
map.on('click', function(e) {
const size = 0.3;
const bounds = L.latLngBounds([
[e.latlng.lat - size, e.latlng.lng - size],
[e.latlng.lat + size, e.latlng.lng + size]
]);
drawBboxRectangle([bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]);
});
}
function drawBboxRectangle(bboxArray) {
const [minLon, minLat, maxLon, maxLat] = bboxArray;
if (rectangle) map.removeLayer(rectangle);
rectangle = L.rectangle([[minLat, minLon], [maxLat, maxLon]], {
color: '#667eea', weight: 2, fillColor: '#667eea', fillOpacity: 0.1
}).addTo(map);
map.fitBounds(rectangle.getBounds());
bbox = bboxArray;
document.getElementById('bboxDisplay').textContent =
`[${minLon.toFixed(4)}, ${minLat.toFixed(4)}, ${maxLon.toFixed(4)}, ${maxLat.toFixed(4)}]`;
updateRunButtonState();
}
async function loadModels() {
try {
const response = await fetch(`${API_BASE}/models/list`);
const data = await response.json();
const modelSelect = document.getElementById('modelSelect');
modelSelect.innerHTML = '<option value="">-- Select a model --</option>';
if (data.models && data.models.length > 0) {
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model.filename;
option.textContent = `${model.filename} (${model.size_mb}MB)`;
modelSelect.appendChild(option);
});
} else {
modelSelect.innerHTML = '<option value="">No trained models found</option>';
}
modelSelect.addEventListener('change', () => {
updateModelInfo();
updateRunButtonState();
});
} catch (error) {
console.error('Error loading models:', error);
document.getElementById('modelSelect').innerHTML = '<option value="">Error loading models</option>';
}
}
function updateModelInfo() {
const modelName = document.getElementById('modelSelect').value;
document.getElementById('modelInfo').textContent = modelName ? `Selected: ${modelName}` : '';
}
function updateRunButtonState() {
const runBtn = document.getElementById('runBtn');
runBtn.disabled = !bbox || !document.getElementById('modelSelect').value;
}
async function runChangeDetection() {
const resultDiv = document.getElementById('resultDiv');
const runBtn = document.getElementById('runBtn');
if (!bbox) {
showResult('error', 'Error', 'Please select an area on the map');
return;
}
const modelFilename = document.getElementById('modelSelect').value;
if (!modelFilename) {
showResult('error', 'Error', 'Please select a trained model');
return;
}
runBtn.disabled = true;
showResult('processing', 'Processing', 'Analyzing land use changes...');
try {
const [minLon, minLat, maxLon, maxLat] = bbox;
const currentStartDate = document.getElementById('currentStartDate').value;
const currentEndDate = document.getElementById('currentEndDate').value;
const predictionStartDate = document.getElementById('predictionStartDate').value;
const predictionEndDate = document.getElementById('predictionEndDate').value;
const maxScenes = parseInt(document.getElementById('maxScenes').value);
const cloudCover = parseInt(document.getElementById('cloudCover').value);
const resolution = parseInt(document.getElementById('resolution').value);
showResult('processing', 'Step 1/3', 'Classifying current period (baseline)...');
const payload = {
model_filename: modelFilename,
min_lon: minLon, min_lat: minLat, max_lon: maxLon, max_lat: maxLat,
current_period: {
start_date: currentStartDate,
end_date: currentEndDate
},
prediction_period: {
start_date: predictionStartDate,
end_date: predictionEndDate
},
max_scenes: maxScenes,
cloud_cover: cloudCover,
resolution: resolution,
export_ndvi: true,
export_classification: true
};
const changeResponse = await fetch(`${API_BASE}/change-detection/compare-periods`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
if (!changeResponse.ok) {
const errorData = await changeResponse.json();
throw new Error(errorData.detail || 'Analysis failed');
}
const changeResult = await changeResponse.json();
displayResults(changeResult);
} catch (error) {
console.error('Error:', error);
showResult('error', 'Error', error.message);
} finally {
runBtn.disabled = false;
}
}
function displayResults(result) {
const resultDiv = document.getElementById('resultDiv');
let html = '<h3 class="success-text">✓ Change Detection Completed</h3>';
// Current period classification
if (result.current_classification) {
const curr = result.current_classification;
html += '<div class="stat-box"><div class="stat-label">📊 Current Period Classification</div>';
html += `<div style="color: #666; font-size: 12px; margin-bottom: 10px;">Scenes: ${curr.n_scenes} | Resolution: ${curr.resolution}m</div>`;
if (curr.class_distribution) {
html += '<table>';
Object.entries(curr.class_distribution).forEach(([cls, count]) => {
const percentage = ((count / Object.values(curr.class_distribution).reduce((a,b) => a+b, 0)) * 100).toFixed(1);
html += `<tr><td>Class ${cls}:</td><td><strong>${count}</strong> (${percentage}%)</td></tr>`;
});
html += '</table>';
}
html += '</div>';
}
// Prediction period classification
if (result.prediction_classification) {
const pred = result.prediction_classification;
html += '<div class="stat-box"><div class="stat-label">🔮 Prediction Period Classification</div>';
html += `<div style="color: #666; font-size: 12px; margin-bottom: 10px;">Scenes: ${pred.n_scenes} | Resolution: ${pred.resolution}m</div>`;
if (pred.class_distribution) {
html += '<table>';
Object.entries(pred.class_distribution).forEach(([cls, count]) => {
const percentage = ((count / Object.values(pred.class_distribution).reduce((a,b) => a+b, 0)) * 100).toFixed(1);
html += `<tr><td>Class ${cls}:</td><td><strong>${count}</strong> (${percentage}%)</td></tr>`;
});
html += '</table>';
}
html += '</div>';
}
// Change detection
if (result.change_detection) {
const cd = result.change_detection;
html += '<div class="stat-box"><div class="stat-label">🔄 Change Detection Summary</div>';
html += `<div class="stat-value" style="color: #e74c3c;">${(cd.change_rate * 100).toFixed(2)}% Changed</div>`;
html += '<table>';
html += '<tr><td>Changed Pixels:</td><td><strong>' + cd.n_changed_pixels.toLocaleString() + '</strong></td></tr>';
html += '<tr><td>Total Pixels:</td><td><strong>' + cd.n_total_pixels.toLocaleString() + '</strong></td></tr>';
html += '</table>';
if (Object.keys(cd.change_matrix).length > 0) {
html += '<div style="margin-top: 10px;"><strong>Transitions (Current → Prediction):</strong></div>';
html += '<pre>' + JSON.stringify(cd.change_matrix, null, 2) + '</pre>';
}
html += '</div>';
}
resultDiv.innerHTML = html;
resultDiv.className = 'result success';
resultDiv.style.display = 'block';
}
function showResult(type, title, message) {
const resultDiv = document.getElementById('resultDiv');
const typeClass = type === 'error' ? 'error' : (type === 'processing' ? 'processing' : 'success');
const textClass = type === 'error' ? 'error-text' : (type === 'processing' ? 'processing-text' : 'success-text');
resultDiv.innerHTML = `<h3 class="${textClass}">${title}</h3><p>${message}</p>` +
(type === 'processing' ? '<div class="progress"><div class="progress-bar" style="animation: progress 2s infinite;"></div></div>' : '');
resultDiv.className = `result ${typeClass}`;
resultDiv.style.display = 'block';
}
document.addEventListener('DOMContentLoaded', () => {
initMap();
loadModels();
});
</script>
</body>
</html>