Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Farm Intrusion & Livestock System</title> | |
| <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> | |
| <meta http-equiv="Pragma" content="no-cache" /> | |
| <meta http-equiv="Expires" content="0" /> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #2ecc71; --primary-dark: #27ae60; --secondary-color: #1e8449; | |
| --accent-color: #3498db; --background-color: #f5f5f5; --card-color: #ffffff; | |
| --text-color: #333333; --text-light: #7f8c8d; --border-color: #e0e0e0; | |
| --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); --border-radius: 8px; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--background-color); color: var(--text-color); display: flex; flex-direction: column; min-height: 100vh; } | |
| .header { background-color: var(--primary-color); color: white; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; box-shadow: var(--shadow); } | |
| .header h1 { font-size: 1.5rem; margin: 0; font-weight: 600; } | |
| .container { display: flex; flex: 1; } | |
| .sidebar { width: 350px; background-color: var(--card-color); border-right: 1px solid var(--border-color); padding: 1.5rem; overflow-y: auto; box-shadow: 2px 0 5px rgba(0,0,0,0.05); } | |
| .main-content { flex: 1; padding: 2rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1.5rem; } | |
| .section-title { color: var(--primary-dark); font-size: 1.2rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 2px solid var(--primary-color); } | |
| .card { background-color: var(--card-color); border-radius: var(--border-radius); box-shadow: var(--shadow); padding: 1.5rem; margin-bottom: 1.5rem; border-top: 4px solid var(--primary-color); } | |
| .video-container { position: relative; overflow: hidden; border-radius: var(--border-radius); background-color: #000; box-shadow: var(--shadow); } | |
| .video-feed { width: 100%; border-radius: var(--border-radius); display: block; } | |
| .status { position: absolute; top: 15px; right: 15px; background-color: rgba(0,0,0,0.6); color: white; padding: 0.5rem 0.75rem; border-radius: 20px; font-size: 0.9rem; display: flex; align-items: center; gap: 8px; } | |
| .status-dot { height: 10px; width: 10px; background-color: var(--primary-color); border-radius: 50%; display: inline-block; animation: pulse 1.5s infinite; } | |
| @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } | |
| .detection-list { list-style: none; margin-top: 0.5rem; } | |
| .detection-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; border-bottom: 1px solid var(--border-color); } | |
| .detection-item:last-child { border-bottom: none; } | |
| .detection-count { background-color: var(--primary-color); color: white; padding: 2px 10px; border-radius: 12px; font-size: 0.9rem; font-weight: bold; } | |
| .alert-info { background-color: rgba(46, 204, 113, 0.1); border-left: 4px solid var(--primary-color); padding: 1rem; margin-top: 1rem; border-radius: 4px; display: flex; align-items: center; gap: 10px; } | |
| .alert-info i { color: var(--primary-color); font-size: 1.2rem; } | |
| #graph { min-height: 250px; } | |
| @media (max-width: 992px) { .container { flex-direction: column; } .sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border-color); } } | |
| @media (max-width: 576px) { .header { padding: 1rem; } .main-content, .sidebar { padding: 1rem; } } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="header"> | |
| <h1><i class="fas fa-video"></i>Farm Intrusion & Livestock Detection</h1> | |
| <div id="current-time"></div> | |
| </header> | |
| <div class="container"> | |
| <div class="sidebar"> | |
| <h2 class="section-title">Detection Summary</h2> | |
| <div class="card"> | |
| <h3 class="section-title">Location</h3> | |
| <div id="location-info">Requesting location access...</div> | |
| </div> | |
| <div class="card"> | |
| <h3 class="section-title">Detected Objects</h3> | |
| <ul id="class-list" class="detection-list"> | |
| <li class="detection-item">Awaiting data...</li> | |
| </ul> | |
| </div> | |
| <div class="card"> | |
| <h3 class="section-title">Detection Graph</h3> | |
| <div id="bar-graph" style="min-height:180px;"></div> | |
| <div id="line-graph" style="min-height:180px;margin-top:20px;"></div> | |
| <div id="pie-graph" style="min-height:180px;margin-top:20px;"></div> | |
| </div> | |
| </div> | |
| <div class="main-content"> | |
| <div class="card"> | |
| <h2 class="section-title">Live Camera Feed</h2> | |
| <div class="video-container"> | |
| <img class="video-feed" src="{{ url_for('video_feed') }}" alt="Live Video Feed"> | |
| <div class="status"><span class="status-dot"></span>Live</div> | |
| </div> | |
| <div class="alert-info"> | |
| <i class="fas fa-bell"></i> | |
| <p>If a person is detected, an automated call will be initiated and a Telegram alert will be sent.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // --- TIME UPDATE --- | |
| const timeElement = document.getElementById('current-time'); | |
| function updateTime() { | |
| timeElement.textContent = new Date().toLocaleTimeString(); | |
| } | |
| setInterval(updateTime, 1000); | |
| updateTime(); | |
| // --- LOCATION HANDLING --- | |
| const locationInfo = document.getElementById('location-info'); | |
| function sendLocationToServer(lat, lon) { | |
| fetch('/set_location', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ latitude: lat, longitude: lon }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.status === 'success' || data.location) { | |
| locationInfo.innerHTML = `<b>Address:</b> ${data.location}<br><b>Latitude:</b> ${data.latitude}<br><b>Longitude:</b> ${data.longitude}`; | |
| } else { | |
| locationInfo.textContent = `Lat: ${lat}, Lon: ${lon}`; | |
| } | |
| }) | |
| .catch(() => { | |
| locationInfo.textContent = 'Could not send location to server.'; | |
| }); | |
| } | |
| if (navigator.geolocation) { | |
| navigator.geolocation.getCurrentPosition( | |
| (position) => { | |
| sendLocationToServer(position.coords.latitude, position.coords.longitude); | |
| }, | |
| () => { | |
| locationInfo.textContent = 'Location access denied.'; | |
| } | |
| ); | |
| } else { | |
| locationInfo.textContent = 'Geolocation is not supported by this browser.'; | |
| } | |
| // --- DATA FETCHING AND UI UPDATES --- | |
| const classList = document.getElementById('class-list'); | |
| async function fetchAndUpdateDashboard() { | |
| try { | |
| const response = await fetch('/object_stats'); | |
| const data = await response.json(); | |
| updateClassList(data.object_counts); | |
| updateGraph(data.object_counts); | |
| // Update location info with full details if available | |
| if (data.location && data.latitude && data.longitude) { | |
| locationInfo.innerHTML = `<b>Address:</b> ${data.location}<br><b>Latitude:</b> ${data.latitude}<br><b>Longitude:</b> ${data.longitude}`; | |
| } | |
| } catch (error) { | |
| classList.innerHTML = '<li class="detection-item">Error loading data</li>'; | |
| } | |
| } | |
| function updateClassList(counts) { | |
| classList.innerHTML = ''; | |
| const objects = Object.keys(counts); | |
| if (objects.length === 0) { | |
| classList.innerHTML = '<li class="detection-item">No objects detected</li>'; | |
| return; | |
| } | |
| objects.forEach(className => { | |
| const li = document.createElement('li'); | |
| li.className = 'detection-item'; | |
| li.innerHTML = `<span>${className.charAt(0).toUpperCase() + className.slice(1)}</span><span class="detection-count">${counts[className]}</span>`; | |
| classList.appendChild(li); | |
| }); | |
| } | |
| // --- GRAPH STATE --- | |
| let timeSeries = []; | |
| let timeLabels = []; | |
| let breakdowns = []; | |
| function updateGraph(counts) { | |
| const classNames = Object.keys(counts); | |
| const objectCounts = Object.values(counts); | |
| const now = new Date().toLocaleTimeString(); | |
| // Save for time-series (limit to 4 bins) | |
| timeLabels.push(now); | |
| timeSeries.push(objectCounts.reduce((a, b) => a + b, 0)); | |
| breakdowns.push(classNames.map((c, i) => `${counts[c]} ${c}`).join(", ")); | |
| if (timeLabels.length > 4) { | |
| timeLabels.shift(); | |
| timeSeries.shift(); | |
| breakdowns.shift(); | |
| } | |
| // Bar chart (top) | |
| const barData = [{ | |
| x: classNames.map(name => name.charAt(0).toUpperCase() + name.slice(1)), | |
| y: objectCounts, | |
| type: 'bar', | |
| marker: { color: 'rgba(46, 204, 113, 0.8)' } | |
| }]; | |
| const barLayout = { | |
| margin: { t: 20, r: 20, l: 40, b: 40 }, | |
| xaxis: { title: 'Detected Objects' }, | |
| yaxis: { title: 'Count', dtick: 1, tickformat: 'd' }, | |
| font: { family: 'Segoe UI, sans-serif' }, | |
| paper_bgcolor: 'transparent', | |
| plot_bgcolor: 'transparent', | |
| }; | |
| Plotly.newPlot('bar-graph', barData, barLayout, { responsive: true, displayModeBar: false }); | |
| // Line chart (middle) | |
| const lineData = [{ | |
| x: [...timeLabels], | |
| y: [...timeSeries], | |
| type: 'scatter', | |
| mode: 'lines+markers', | |
| name: 'Total Objects', | |
| hovertemplate: '%{y} objects<br>%{text}', | |
| text: breakdowns, | |
| line: { color: 'rgba(39, 174, 96, 1)', width: 3 }, | |
| marker: { color: 'rgba(39, 174, 96, 1)', size: 8 } | |
| }]; | |
| const lineLayout = { | |
| margin: { t: 20, r: 20, l: 40, b: 40 }, | |
| xaxis: { title: 'Time', tickmode: 'array', tickvals: [...timeLabels], ticktext: [...timeLabels], automargin: true }, | |
| yaxis: { title: 'Total Object Count', dtick: 1, tickformat: 'd' }, | |
| font: { family: 'Segoe UI, sans-serif' }, | |
| paper_bgcolor: 'transparent', | |
| plot_bgcolor: 'transparent', | |
| hovermode: 'closest', | |
| }; | |
| Plotly.newPlot('line-graph', lineData, lineLayout, { responsive: true, displayModeBar: false }); | |
| // Pie chart (bottom) | |
| const pieData = [{ | |
| labels: classNames.map(name => name.charAt(0).toUpperCase() + name.slice(1)), | |
| values: objectCounts, | |
| type: 'pie', | |
| textinfo: 'label+percent', | |
| insidetextorientation: 'radial', | |
| marker: { colors: ['#2ecc71', '#27ae60', '#3498db', '#1e8449', '#e67e22', '#e74c3c'] } | |
| }]; | |
| const pieLayout = { | |
| margin: { t: 20, r: 20, l: 40, b: 40 }, | |
| font: { family: 'Segoe UI, sans-serif' }, | |
| paper_bgcolor: 'transparent', | |
| plot_bgcolor: 'transparent', | |
| }; | |
| Plotly.newPlot('pie-graph', pieData, pieLayout, { responsive: true, displayModeBar: false }); | |
| } | |
| // Initial call and set interval to fetch every 1 second | |
| fetchAndUpdateDashboard(); | |
| setInterval(fetchAndUpdateDashboard, 1000); | |
| }); | |
| </script> | |
| </body> | |
| </html> |