CSNET/thairice-ndvi-visualizer
0
1// ThaiRice NDVI Visualizer - Main JavaScript2 3// Initialize application4document.addEventListener('DOMContentLoaded', function() {5 console.log('ThaiRice NDVI Visualizer initialized');6 7 // Initialize components8 initNDVIInteractions();9 initDateDisplay();10 initHealthScoreAnimation();11 initChartSimulation();12 13 // Set up service worker for offline functionality14 if ('serviceWorker' in navigator) {15 navigator.serviceWorker.register('/sw.js').catch(console.error);16 }17});18 19// NDVI Map Interactions20function initNDVIInteractions() {21 const mapControls = document.querySelectorAll('.map-control');22 const ndviImage = document.querySelector('img[alt="Rice Field NDVI Map"]');23 24 mapControls.forEach(control => {25 control.addEventListener('click', function(e) {26 e.preventDefault();27 const action = this.querySelector('i').getAttribute('data-feather');28 29 switch(action) {30 case 'zoom-in':31 simulateZoom(1.2);32 break;33 case 'zoom-out':34 simulateZoom(0.8);35 break;36 case 'maximize':37 toggleFullscreen(ndviImage);38 break;39 }40 41 // Add click feedback42 this.classList.add('scale-95');43 setTimeout(() => this.classList.remove('scale-95'), 150);44 });45 });46}47 48// Simulate zoom effect49function simulateZoom(factor) {50 const mapContainer = document.querySelector('.relative.h-96');51 const img = mapContainer.querySelector('img');52 53 // Store current transform54 const currentTransform = img.style.transform || 'scale(1)';55 const currentScale = parseFloat(currentTransform.match(/scale\(([^)]+)\)/)?.[1] || 1);56 const newScale = currentScale * factor;57 58 // Limit zoom range59 if (newScale < 0.5 || newScale > 3) return;60 61 img.style.transform = `scale(${newScale})`;62 img.style.transition = 'transform 0.3s ease';63 64 // Update controls state65 updateZoomControls(newScale);66}67 68function updateZoomControls(scale) {69 const zoomIn = document.querySelector('[data-feather="zoom-in"]').closest('button');70 const zoomOut = document.querySelector('[data-feather="zoom-out"]').closest('button');71 72 zoomIn.disabled = scale >= 3;73 zoomOut.disabled = scale <= 0.5;74}75 76function toggleFullscreen(element) {77 if (!document.fullscreenElement) {78 if (element.requestFullscreen) {79 element.requestFullscreen();80 } else if (element.webkitRequestFullscreen) {81 element.webkitRequestFullscreen();82 } else if (element.msRequestFullscreen) {83 element.msRequestFullscreen();84 }85 } else {86 if (document.exitFullscreen) {87 document.exitFullscreen();88 } else if (document.webkitExitFullscreen) {89 document.webkitExitFullscreen();90 } else if (document.msExitFullscreen) {91 document.msExitFullscreen();92 }93 }94}95 96// Initialize date display97function initDateDisplay() {98 const dateElements = document.querySelectorAll('[data-date-format]');99 const now = new Date();100 101 dateElements.forEach(element => {102 const format = element.getAttribute('data-date-format');103 let formattedDate;104 105 switch(format) {106 case 'relative':107 formattedDate = getRelativeDate(now);108 break;109 case 'full':110 formattedDate = now.toLocaleDateString('en-US', {111 weekday: 'long',112 year: 'numeric',113 month: 'long',114 day: 'numeric'115 });116 break;117 default:118 formattedDate = now.toLocaleDateString();119 }120 121 if (element.tagName === 'INPUT') {122 element.value = formattedDate;123 } else {124 element.textContent = formattedDate;125 }126 });127}128 129function getRelativeDate(date) {130 const now = new Date();131 const diffMs = now - date;132 const diffMins = Math.floor(diffMs / 60000);133 const diffHours = Math.floor(diffMins / 60);134 const diffDays = Math.floor(diffHours / 24);135 136 if (diffMins < 1) return 'Just now';137 if (diffMins < 60) return `${diffMins} minutes ago`;138 if (diffHours < 24) return `${diffHours} hours ago`;139 if (diffDays === 1) return 'Yesterday';140 if (diffDays < 7) return `${diffDays} days ago`;141 142 return date.toLocaleDateString();143}144 145// Animate health score146function initHealthScoreAnimation() {147 const healthScore = document.querySelector('.inline-flex.items-center.justify-center');148 if (!healthScore) return;149 150 // Add animation class151 healthScore.classList.add('pulse-slow');152 153 // Simulate real-time updates154 setInterval(() => {155 const randomChange = Math.random() * 0.02 - 0.01; // +/- 1%156 const currentScore = parseFloat(healthScore.querySelector('span').textContent);157 const newScore = Math.min(100, Math.max(0, currentScore + randomChange));158 159 healthScore.querySelector('span').textContent = `${newScore.toFixed(1)}%`;160 161 // Update gradient based on score162 updateHealthGradient(newScore);163 }, 10000); // Update every 10 seconds164}165 166function updateHealthGradient(score) {167 const healthIndicator = document.querySelector('.health-indicator');168 if (!healthIndicator) return;169 170 let color;171 if (score >= 80) color = '#10b981'; // Green172 else if (score >= 60) color = '#f59e0b'; // Amber173 else color = '#ef4444'; // Red174 175 healthIndicator.style.setProperty('--indicator-color', color);176}177 178// Simulate chart data updates179function initChartSimulation() {180 const chartBars = document.querySelectorAll('.bg-gradient-to-t');181 if (!chartBars.length) return;182 183 // Animate bars on load184 setTimeout(() => {185 chartBars.forEach((bar, index) => {186 bar.style.height = bar.style.height || getComputedStyle(bar).height;187 bar.style.height = '0';188 189 setTimeout(() => {190 bar.style.transition = 'height 0.8s ease';191 bar.style.height = bar.dataset.originalHeight || bar.style.height;192 }, index * 100);193 });194 }, 500);195 196 // Simulate real-time updates197 setInterval(() => {198 const currentNDVI = document.querySelector('.text-2xl.font-bold.text-green-700');199 if (!currentNDVI) return;200 201 const currentValue = parseFloat(currentNDVI.textContent);202 const randomChange = (Math.random() * 0.04 - 0.02); // +/- 0.02203 const newValue = Math.min(0.9, Math.max(0.1, currentValue + randomChange));204 205 // Animate the change206 animateValue(currentNDVI, currentValue, newValue, 1000);207 208 // Update trend indicator209 updateTrendIndicator(randomChange);210 211 }, 15000); // Update every 15 seconds212}213 214function animateValue(element, start, end, duration) {215 const startTime = performance.now();216 217 function update(currentTime) {218 const elapsed = currentTime - startTime;219 const progress = Math.min(elapsed / duration, 1);220 221 const current = start + (end - start) * progress;222 element.textContent = current.toFixed(2);223 224 if (progress < 1) {225 requestAnimationFrame(update);226 }227 }228 229 requestAnimationFrame(update);230}231 232function updateTrendIndicator(change) {233 const trendElement = document.querySelector('.text-xs.text-green-600.mt-1');234 if (!trendElement) return;235 236 const icon = trendElement.querySelector('i');237 const text = trendElement.querySelector('span') || trendElement;238 239 if (change > 0) {240 icon.setAttribute('data-feather', 'trending-up');241 text.textContent = `+${Math.abs(change).toFixed(2)} from last update`;242 trendElement.className = 'text-xs text-green-600 mt-1';243 } else if (change < 0) {244 icon.setAttribute('data-feather', 'trending-down');245 text.textContent = `-${Math.abs(change).toFixed(2)} from last update`;246 trendElement.className = 'text-xs text-red-600 mt-1';247 } else {248 icon.setAttribute('data-feather', 'minus');249 text.textContent = 'No change from last update';250 trendElement.className = 'text-xs text-gray-600 mt-1';251 }252 253 feather.replace();254}255 256// Export functionality257function exportNDVIReport() {258 const reportData = {259 fieldId: 'TH-RF-2023-042',260 location: 'Suphan Buri, Thailand',261 area: '15.3 hectares',262 ndviScore: document.querySelector('.text-4xl.font-bold.text-white')?.textContent || '82%',263 currentNDVI: document.querySelector('.text-2xl.font-bold.text-green-700')?.textContent || '0.62',264 lastUpdated: new Date().toISOString(),265 recommendations: Array.from(document.querySelectorAll('.bg-gradient-to-br')).map(card => ({266 category: card.querySelector('h3')?.textContent,267 status: card.querySelector('p.text-sm')?.textContent,268 details: card.querySelector('p.text-gray-700')?.textContent269 }))270 };271 272 // Create download link273 const dataStr = JSON.stringify(reportData, null, 2);274 const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);275 276 const exportFileDefaultName = `ndvi-report-${reportData.fieldId}-${new Date().toISOString().split('T')[0]}.json`;277 278 const linkElement = document.createElement('a');279 linkElement.setAttribute('href', dataUri);280 linkElement.setAttribute('download', exportFileDefaultName);281 linkElement.click();282 283 // Show notification284 showNotification('Report exported successfully!', 'success');285}286 287// Notification system288function showNotification(message, type = 'info') {289 const notification = document.createElement('div');290 notification.className = `fixed top-4 right-4 z-50 px-6 py-4 rounded-lg shadow-lg transform transition-transform duration-300 translate-x-full ${291 type === 'success' ? 'bg-green-500 text-white' :292 type === 'error' ? 'bg-red-500 text-white' :293 'bg-blue-500 text-white'294 }`;295 notification.textContent = message;296 297 document.body.appendChild(notification);298 299 // Animate in300 setTimeout(() => {301 notification.classList.remove('translate-x-full');302 }, 10);303 304 // Auto remove after 5 seconds305 setTimeout(() => {306 notification.classList.add('translate-x-full');307 setTimeout(() => notification.remove(), 300);308 }, 5000);309}310 311// Error handling312window.addEventListener('error', function(e) {313 console.error('Application error:', e.error);314 showNotification('An error occurred. Please refresh the page.', 'error');315});316 317// Performance monitoring318if ('performance' in window) {319 window.addEventListener('load', function() {320 const timing = performance.timing;321 const loadTime = timing.loadEventEnd - timing.navigationStart;322 console.log(`Page loaded in ${loadTime}ms`);323 324 if (loadTime > 3000) {325 console.warn('Page load time exceeds 3 seconds');326 }327 });328}