Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>🚁 DroneAgent - Waypoint Editor</title> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |
| <style> | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .container { | |
| display: flex; | |
| height: 100vh; | |
| } | |
| .sidebar { | |
| width: 350px; | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| padding: 20px; | |
| overflow-y: auto; | |
| box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); | |
| } | |
| .map-container { | |
| flex: 1; | |
| position: relative; | |
| } | |
| #map { | |
| height: 100%; | |
| width: 100%; | |
| } | |
| .header { | |
| background: rgba(255, 255, 255, 0.2); | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| text-align: center; | |
| } | |
| .waypoint-item { | |
| background: rgba(255, 255, 255, 0.1); | |
| padding: 10px; | |
| margin: 8px 0; | |
| border-radius: 8px; | |
| border-left: 4px solid #4CAF50; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .waypoint-item:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| transform: translateX(5px); | |
| } | |
| .waypoint-item.active { | |
| background: rgba(76, 175, 80, 0.3); | |
| border-left-color: #4CAF50; | |
| } | |
| .btn { | |
| background: linear-gradient(45deg, #4CAF50, #45a049); | |
| color: white; | |
| border: none; | |
| padding: 12px 20px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: bold; | |
| transition: all 0.3s ease; | |
| width: 100%; | |
| margin: 8px 0; | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3); | |
| } | |
| .btn:disabled { | |
| background: #666; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .status { | |
| padding: 10px; | |
| border-radius: 8px; | |
| margin: 10px 0; | |
| font-weight: bold; | |
| } | |
| .status.success { | |
| background: rgba(76, 175, 80, 0.2); | |
| border-left: 4px solid #4CAF50; | |
| } | |
| .status.error { | |
| background: rgba(244, 67, 54, 0.2); | |
| border-left: 4px solid #f44336; | |
| } | |
| .status.info { | |
| background: rgba(33, 150, 243, 0.2); | |
| border-left: 4px solid #2196F3; | |
| } | |
| .coordinate-display { | |
| font-family: 'Courier New', monospace; | |
| background: rgba(0, 0, 0, 0.2); | |
| padding: 8px; | |
| border-radius: 4px; | |
| margin: 5px 0; | |
| font-size: 12px; | |
| } | |
| .loading { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="sidebar"> | |
| <div class="header"> | |
| <h2>🚁 Waypoint Editor</h2> | |
| <p>Edit mission waypoints on the map</p> | |
| </div> | |
| <div id="status" class="status info"> | |
| Click "Load Mission" to get started | |
| </div> | |
| <button id="loadBtn" class="btn">📥 Load Mission Waypoints</button> | |
| <button id="saveBtn" class="btn" disabled>💾 Confirm Flight with Edited Waypoints</button> | |
| <button id="resetBtn" class="btn" disabled>🔄 Reset Changes</button> | |
| <div id="waypointsList"> | |
| <!-- Waypoints will be loaded here --> | |
| </div> | |
| <div id="coordinatesDisplay" style="margin-top: 20px;"> | |
| <!-- Coordinate details will be shown here --> | |
| </div> | |
| </div> | |
| <div class="map-container"> | |
| <div id="map"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize map | |
| const map = L.map('map').setView([16.047, 108.206], 15); | |
| // Add OpenStreetMap tiles | |
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
| attribution: '© OpenStreetMap contributors' | |
| }).addTo(map); | |
| // Store waypoints and markers | |
| let waypoints = []; | |
| let markers = []; | |
| let originalWaypoints = []; | |
| let selectedWaypointIndex = -1; | |
| // API functions | |
| async function loadWaypoints() { | |
| try { | |
| updateStatus('Loading waypoints...', 'info'); | |
| showLoading(true, 'loadBtn'); | |
| const response = await fetch('/mission/waypoints'); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| waypoints = data.waypoints; | |
| originalWaypoints = JSON.parse(JSON.stringify(waypoints)); // Deep copy | |
| displayWaypoints(); | |
| updateMapMarkers(); | |
| updateStatus(`✅ Loaded ${waypoints.length} waypoints`, 'success'); | |
| document.getElementById('saveBtn').disabled = false; | |
| document.getElementById('resetBtn').disabled = false; | |
| } else { | |
| updateStatus(`❌ ${data.message}`, 'error'); | |
| } | |
| } catch (error) { | |
| updateStatus(`❌ Error loading waypoints: ${error.message}`, 'error'); | |
| } finally { | |
| showLoading(false, 'loadBtn'); | |
| } | |
| } | |
| async function confirmFlight() { | |
| try { | |
| updateStatus('Confirming flight...', 'info'); | |
| showLoading(true, 'saveBtn'); | |
| const confirmData = { | |
| mission_id: `edited_mission_${Date.now()}`, | |
| drone_id: 'drone_001', | |
| waypoints: waypoints, | |
| altitude: 70.0 | |
| }; | |
| const response = await fetch('/flight/confirm', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(confirmData) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'confirmed' || response.ok) { | |
| updateStatus('✅ Flight confirmed with edited waypoints!', 'success'); | |
| alert('🎉 Flight confirmed successfully!\n\nYour edited waypoints have been saved and the flight mission is ready.'); | |
| } else { | |
| updateStatus(`❌ Confirmation failed: ${data.message}`, 'error'); | |
| } | |
| } catch (error) { | |
| updateStatus(`❌ Error confirming flight: ${error.message}`, 'error'); | |
| } finally { | |
| showLoading(false, 'saveBtn'); | |
| } | |
| } | |
| function resetChanges() { | |
| waypoints = JSON.parse(JSON.stringify(originalWaypoints)); | |
| displayWaypoints(); | |
| updateMapMarkers(); | |
| updateStatus('🔄 Changes reset to original waypoints', 'info'); | |
| } | |
| function displayWaypoints() { | |
| const list = document.getElementById('waypointsList'); | |
| list.innerHTML = '<h3>📍 Waypoints</h3>'; | |
| waypoints.forEach((wp, index) => { | |
| const item = document.createElement('div'); | |
| item.className = 'waypoint-item'; | |
| item.onclick = () => selectWaypoint(index); | |
| const latDiff = originalWaypoints[index] ? | |
| (wp.lat - originalWaypoints[index].lat).toFixed(6) : '0.000000'; | |
| const lngDiff = originalWaypoints[index] ? | |
| (wp.lng - originalWaypoints[index].lng).toFixed(6) : '0.000000'; | |
| const hasChanged = Math.abs(latDiff) > 0.000001 || Math.abs(lngDiff) > 0.000001; | |
| item.innerHTML = ` | |
| <strong>Waypoint ${index + 1}</strong><br> | |
| <div class="coordinate-display"> | |
| Lat: ${wp.lat.toFixed(8)}<br> | |
| Lng: ${wp.lng.toFixed(8)} | |
| </div> | |
| ${hasChanged ? '<small style="color: #FFC107;">✏️ Modified</small>' : '<small style="color: #4CAF50;">✅ Original</small>'} | |
| `; | |
| list.appendChild(item); | |
| }); | |
| } | |
| function selectWaypoint(index) { | |
| selectedWaypointIndex = index; | |
| // Update UI | |
| document.querySelectorAll('.waypoint-item').forEach((item, i) => { | |
| item.classList.toggle('active', i === index); | |
| }); | |
| // Pan map to selected waypoint | |
| if (waypoints[index]) { | |
| map.setView([waypoints[index].lat, waypoints[index].lng], 18); | |
| } | |
| updateCoordinateDisplay(); | |
| } | |
| function updateCoordinateDisplay() { | |
| const display = document.getElementById('coordinatesDisplay'); | |
| if (selectedWaypointIndex >= 0 && waypoints[selectedWaypointIndex]) { | |
| const wp = waypoints[selectedWaypointIndex]; | |
| const original = originalWaypoints[selectedWaypointIndex]; | |
| display.innerHTML = ` | |
| <h4>🎯 Selected Waypoint ${selectedWaypointIndex + 1}</h4> | |
| <div class="coordinate-display"> | |
| <strong>Current:</strong><br> | |
| Latitude: ${wp.lat}<br> | |
| Longitude: ${wp.lng} | |
| </div> | |
| ${original ? ` | |
| <div class="coordinate-display"> | |
| <strong>Original:</strong><br> | |
| Latitude: ${original.lat}<br> | |
| Longitude: ${original.lng} | |
| </div> | |
| <div class="coordinate-display"> | |
| <strong>Difference:</strong><br> | |
| Lat: ${(wp.lat - original.lat).toFixed(8)}<br> | |
| Lng: ${(wp.lng - original.lng).toFixed(8)} | |
| </div> | |
| ` : ''} | |
| `; | |
| } else { | |
| display.innerHTML = '<p style="color: #999;">Click a waypoint to see details</p>'; | |
| } | |
| } | |
| function updateMapMarkers() { | |
| // Clear existing markers | |
| markers.forEach(marker => map.removeLayer(marker)); | |
| markers = []; | |
| // Add new markers | |
| waypoints.forEach((wp, index) => { | |
| const marker = L.marker([wp.lat, wp.lng], { | |
| draggable: true, | |
| title: `Waypoint ${index + 1}` | |
| }); | |
| // Create custom icon with waypoint number | |
| const customIcon = L.divIcon({ | |
| className: 'waypoint-marker', | |
| html: `<div style=" | |
| background: #4CAF50; | |
| color: white; | |
| border-radius: 50%; | |
| width: 30px; | |
| height: 30px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: bold; | |
| border: 2px solid white; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.3); | |
| ">${index + 1}</div>`, | |
| iconSize: [30, 30], | |
| iconAnchor: [15, 15] | |
| }); | |
| marker.setIcon(customIcon); | |
| // Handle drag events | |
| marker.on('dragend', function(event) { | |
| const newPos = event.target.getLatLng(); | |
| waypoints[index] = { | |
| lat: newPos.lat, | |
| lng: newPos.lng | |
| }; | |
| displayWaypoints(); | |
| updateCoordinateDisplay(); | |
| updateStatus('📍 Waypoint moved - click Save to confirm', 'info'); | |
| }); | |
| marker.addTo(map); | |
| markers.push(marker); | |
| }); | |
| // Fit map to show all waypoints | |
| if (waypoints.length > 0) { | |
| const bounds = L.latLngBounds(waypoints.map(wp => [wp.lat, wp.lng])); | |
| map.fitBounds(bounds, { padding: [20, 20] }); | |
| } | |
| } | |
| function updateStatus(message, type) { | |
| const status = document.getElementById('status'); | |
| status.textContent = message; | |
| status.className = `status ${type}`; | |
| } | |
| function showLoading(show, buttonId) { | |
| const button = document.getElementById(buttonId); | |
| const loading = button.querySelector('.loading'); | |
| if (show) { | |
| if (!loading) { | |
| button.innerHTML = '<div class="loading"></div> Loading...'; | |
| } | |
| button.disabled = true; | |
| } else { | |
| if (loading) { | |
| button.innerHTML = button.innerHTML.replace('<div class="loading"></div> Loading...', button.textContent); | |
| } | |
| button.disabled = false; | |
| } | |
| } | |
| // Event listeners | |
| document.getElementById('loadBtn').addEventListener('click', loadWaypoints); | |
| document.getElementById('saveBtn').addEventListener('click', confirmFlight); | |
| document.getElementById('resetBtn').addEventListener('click', resetChanges); | |
| // Initialize | |
| updateStatus('🚁 Welcome to Waypoint Editor! Click "Load Mission" to get started.', 'info'); | |
| </script> | |
| </body> | |
| </html> | |