hoàn thành chức năng predict ndvi time series analysis
This commit is contained in:
+87
-22
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NDVI Time Series Analysis</title>
|
||||
<title>NDVI Time Series Predicts</title>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
@@ -131,6 +131,22 @@
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.region-filter-btn {
|
||||
padding: 3px 10px;
|
||||
background: #e0e0e0;
|
||||
color: #666;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.region-filter-btn.active {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 400px;
|
||||
@@ -199,7 +215,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🌿 NDVI Time Series Analysis</h1>
|
||||
<h1>🌿 NDVI Time Series Predicts</h1>
|
||||
<p>Phân tích chỉ số thực vật theo thời gian từ dữ liệu Sentinel-2</p>
|
||||
</div>
|
||||
|
||||
@@ -208,7 +224,7 @@
|
||||
<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</a>
|
||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis (Active)</a>
|
||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Predicts (Active)</a>
|
||||
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
||||
</div>
|
||||
|
||||
@@ -251,7 +267,7 @@
|
||||
|
||||
<!-- Region Filters -->
|
||||
<div id="regionFilterContainer" style="display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 10px;">
|
||||
<button type="button" class="region-filter-btn" data-region="all" style="padding: 3px 10px; background: #667eea; color: white; border: none; border-radius: 15px; font-size: 0.85em; cursor: pointer;">Tất cả</button>
|
||||
<button type="button" class="region-filter-btn active" data-region="all">Tất cả</button>
|
||||
</div>
|
||||
|
||||
<select id="provinceSelect" style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 5px;">
|
||||
@@ -397,7 +413,18 @@
|
||||
let allProvincesMerged = {};
|
||||
let currentProvinceName = '';
|
||||
let currentProvinceMode = '63';
|
||||
let currentRegionFilter = 'all';
|
||||
let currentAnalysisMode = 'historical';
|
||||
|
||||
// Check if date range goes into the future
|
||||
function isFutureRange(startDate, endDate) {
|
||||
if (!startDate || !endDate) return false;
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
return start > today || end > today;
|
||||
}
|
||||
|
||||
// Switch between historical and forecast mode
|
||||
function switchMode(mode) {
|
||||
@@ -453,6 +480,25 @@
|
||||
|
||||
// Execute analysis based on current mode
|
||||
async function executeAnalysis() {
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
showError('Vui lòng chọn khoảng thời gian!');
|
||||
return;
|
||||
}
|
||||
|
||||
// If user picks a future range while in historical mode, switch to forecast automatically
|
||||
if (currentAnalysisMode === 'historical' && isFutureRange(startDate, endDate)) {
|
||||
switchMode('forecast');
|
||||
const methodAlert = document.getElementById('methodAlert');
|
||||
methodAlert.innerHTML = '<strong>🔄 Đã chuyển sang chế độ Dự đoán tương lai</strong><br>' +
|
||||
'Khoảng thời gian nằm trong tương lai, hệ thống tự dùng mô hình forecast để tránh lỗi "No Sentinel-2 data".';
|
||||
document.getElementById('errorContainer').style.display = 'none';
|
||||
await forecastNDVI();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentAnalysisMode === 'historical') {
|
||||
await predictNDVI();
|
||||
} else {
|
||||
@@ -682,10 +728,11 @@
|
||||
|
||||
// Update stats - handle both n_images (historical) and n_forecast_points (forecast)
|
||||
const nImages = data.n_images || data.n_forecast_points || 0;
|
||||
const safeStat = (val) => (val === undefined || val === null || Number.isNaN(val) ? '-' : Number(val).toFixed(3));
|
||||
document.getElementById('statImages').textContent = nImages;
|
||||
document.getElementById('statAvgNDVI').textContent = data.mean_ndvi.toFixed(3);
|
||||
document.getElementById('statMinNDVI').textContent = data.min_ndvi.toFixed(3);
|
||||
document.getElementById('statMaxNDVI').textContent = data.max_ndvi.toFixed(3);
|
||||
document.getElementById('statAvgNDVI').textContent = safeStat(data.mean_ndvi);
|
||||
document.getElementById('statMinNDVI').textContent = safeStat(data.min_ndvi);
|
||||
document.getElementById('statMaxNDVI').textContent = safeStat(data.max_ndvi);
|
||||
|
||||
// Create chart
|
||||
createChart(data);
|
||||
@@ -699,6 +746,21 @@
|
||||
if (ndviChart) {
|
||||
ndviChart.destroy();
|
||||
}
|
||||
|
||||
if (!data.timeseries || data.timeseries.length === 0) {
|
||||
showError('Không có dữ liệu để vẽ biểu đồ.');
|
||||
return;
|
||||
}
|
||||
|
||||
const pickValue = (item, keys) => {
|
||||
for (const key of keys) {
|
||||
const val = item[key];
|
||||
if (val !== undefined && val !== null && !Number.isNaN(val)) {
|
||||
return Number(val);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Format dates with more detail (dd/MM/yyyy)
|
||||
const dates = data.timeseries.map(item => {
|
||||
@@ -708,9 +770,9 @@
|
||||
const year = d.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
});
|
||||
const ndviValues = data.timeseries.map(item => item.mean_ndvi);
|
||||
const minValues = data.timeseries.map(item => item.min_ndvi);
|
||||
const maxValues = data.timeseries.map(item => item.max_ndvi);
|
||||
const ndviValues = data.timeseries.map(item => pickValue(item, ['mean_ndvi', 'ndvi_mean', 'ndvi']));
|
||||
const minValues = data.timeseries.map(item => pickValue(item, ['min_ndvi', 'ndvi_min']));
|
||||
const maxValues = data.timeseries.map(item => pickValue(item, ['max_ndvi', 'ndvi_max']));
|
||||
|
||||
ndviChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
@@ -837,7 +899,7 @@
|
||||
allProvinces = await response63.json();
|
||||
allProvincesMerged = await response32.json();
|
||||
|
||||
populateProvinceSelect();
|
||||
populateProvinceSelect(currentRegionFilter);
|
||||
populateRegionButtons();
|
||||
} catch (error) {
|
||||
console.error('Error loading provinces:', error);
|
||||
@@ -951,10 +1013,19 @@
|
||||
button.className = 'region-filter-btn';
|
||||
button.dataset.region = region;
|
||||
button.textContent = region;
|
||||
button.style.cssText = 'padding: 3px 10px; background: #e0e0e0; color: #666; border: none; border-radius: 15px; font-size: 0.85em; cursor: pointer;';
|
||||
if (region === currentRegionFilter) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
container.appendChild(button);
|
||||
});
|
||||
|
||||
// Re-apply active state for "Tất cả"
|
||||
if (currentRegionFilter === 'all') {
|
||||
allButton.classList.add('active');
|
||||
} else {
|
||||
allButton.classList.remove('active');
|
||||
}
|
||||
|
||||
setupRegionFilters();
|
||||
}
|
||||
|
||||
@@ -964,16 +1035,10 @@
|
||||
|
||||
filterButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
filterButtons.forEach(b => {
|
||||
b.style.background = '#e0e0e0';
|
||||
b.style.color = '#666';
|
||||
});
|
||||
|
||||
this.style.background = '#27ae60';
|
||||
this.style.color = 'white';
|
||||
|
||||
const region = this.dataset.region;
|
||||
populateProvinceSelect(region);
|
||||
filterButtons.forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
currentRegionFilter = this.dataset.region;
|
||||
populateProvinceSelect(currentRegionFilter);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user