zok213 commited on
Commit
6740714
·
1 Parent(s): 9c611f7

Here's a summary of the changes:

Browse files

.gitignore: Updated to exclude test files and documentation.
routes.py: Added a new endpoint /mission/waypoints to retrieve mission waypoints for map editing.
ai.py: Updated with the latest AI logic.
geocoding.py: Updated with the latest geocoding logic.
drone_agent.py: Updated with the latest drone agent logic.
mission_planner.py: Updated with the latest mission planner logic.
waypoint_editor.html: New file to provide a user interface for editing waypoints on a map.

.gitignore CHANGED
@@ -1,31 +1,77 @@
1
- mavproxy
2
- *~
3
- *.o
4
- *.pyc
5
- *.log
6
- camera/
7
  build/
 
8
  dist/
9
- gtest/
10
- *.tlog
11
- *.raw
 
 
 
 
 
 
 
 
 
12
  MANIFEST
13
- *.egg-info
14
- .DS_Store
15
- *.bak
16
- *.orig
17
- windows/version.txt
18
- /.vscode
19
- *node_modules/
20
- *node_modules
21
- cleanup_backup_*
22
- *venv/
23
- *venv
24
- *logs/
25
-
26
- *.backup*
 
 
 
 
 
 
27
  *.backup
 
 
 
 
 
 
 
 
 
 
 
 
28
 
 
 
 
 
 
 
 
 
 
 
 
29
  .env
30
- *.env
31
- .env.fixed
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
  build/
8
+ develop-eggs/
9
  dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
  MANIFEST
23
+
24
+ # Virtual environments
25
+ venv/
26
+ env/
27
+ ENV/
28
+ .venv/
29
+ .env/
30
+
31
+ # IDE
32
+ .vscode/
33
+ .idea/
34
+ *.swp
35
+ *.swo
36
+
37
+ # Logs
38
+ *.log
39
+ *.out
40
+ nohup.out
41
+
42
+ # Backup and temporary files
43
  *.backup
44
+ *.backup.*
45
+ *~
46
+ *.orig
47
+ *.bak
48
+ *.tmp
49
+
50
+ # Mission files (generated at runtime)
51
+ downloads/*.plan
52
+
53
+ # Chat testing files
54
+ chat_testing/
55
+ *.test.json
56
 
57
+ # MAVProxy and ArduPilot
58
+ *.tlog
59
+ *.tlog.raw
60
+ eeprom.bin
61
+ sitl.pid
62
+
63
+ # OS
64
+ .DS_Store
65
+ Thumbs.db
66
+
67
+ # Deployment
68
  .env
69
+ .env.local
70
+ .env.production
71
+ .env.fixed
72
+ archive_old_backends
73
+ *test*
74
+ *simulator*
75
+
76
+ # Documentation (moved to docs/ folder - ignored to reduce repository size)
77
+ docs/
src/api/routes.py CHANGED
@@ -170,16 +170,143 @@ async def serve_chatbot():
170
  return FileResponse("chatbot.html")
171
 
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  @router.post("/flight/confirm")
174
  async def confirm_flight(mission_data: dict):
175
  """Confirm and start tracking a flight mission.
176
-
177
  Expects JSON with:
178
  - mission_id: str
179
  - drone_id: str
180
- - waypoints: list of {"lat": float, "lon": float, "alt": float}
 
 
181
  """
182
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  result = await flight_manager.confirm_flight(mission_data)
184
  return result
185
  except Exception as e:
 
170
  return FileResponse("chatbot.html")
171
 
172
 
173
+ @router.get("/mission/waypoints")
174
+ async def get_mission_waypoints(filename: str = None, latest: bool = True):
175
+ """Get mission waypoints for map editing.
176
+
177
+ Returns waypoints in format suitable for map editing:
178
+ [
179
+ { lat: 16.010361, lng: 108.258056 },
180
+ { lat: 16.012904326313578, lng: 108.25862196878661 },
181
+ ...
182
+ ]
183
+
184
+ Args:
185
+ filename: Specific mission file name (optional)
186
+ latest: Get latest mission if filename not provided
187
+ """
188
+ try:
189
+ from pathlib import Path
190
+ import json
191
+
192
+ downloads_dir = Path("downloads")
193
+ if not downloads_dir.exists():
194
+ return {
195
+ "status": "error",
196
+ "message": "No missions found. Generate a mission first!",
197
+ "waypoints": []
198
+ }
199
+
200
+ # Find the mission file
201
+ if filename:
202
+ mission_file = downloads_dir / filename
203
+ if not mission_file.exists():
204
+ return {
205
+ "status": "error",
206
+ "message": f"Mission file {filename} not found",
207
+ "waypoints": []
208
+ }
209
+ else:
210
+ # Get latest .plan file
211
+ plan_files = list(downloads_dir.glob("*.plan"))
212
+ if not plan_files:
213
+ return {
214
+ "status": "error",
215
+ "message": "No mission files found. Generate a mission first!",
216
+ "waypoints": []
217
+ }
218
+ mission_file = max(plan_files, key=lambda f: f.stat().st_mtime)
219
+
220
+ # Read and parse the mission file
221
+ with open(mission_file, 'r', encoding='utf-8') as f:
222
+ mission_data = json.load(f)
223
+
224
+ # Extract waypoints from mission items (skip takeoff and landing)
225
+ waypoints = []
226
+ mission_items = mission_data.get("mission", {}).get("items", [])
227
+
228
+ for item in mission_items:
229
+ # Only include waypoint items (command 16 = MAV_CMD_NAV_WAYPOINT)
230
+ if item.get("command") == 16: # MAV_CMD_NAV_WAYPOINT
231
+ params = item.get("params", [])
232
+ if len(params) >= 7: # lat, lon are params[4] and [5]
233
+ # Return exact coordinates without rounding
234
+ waypoint = {
235
+ "lat": params[4], # Exact latitude
236
+ "lng": params[5] # Exact longitude (using lng as requested)
237
+ }
238
+ waypoints.append(waypoint)
239
+
240
+ return {
241
+ "status": "success",
242
+ "message": f"Retrieved {len(waypoints)} waypoints from {mission_file.name}",
243
+ "filename": mission_file.name,
244
+ "waypoints": waypoints,
245
+ "mission_info": {
246
+ "cruise_speed": mission_data.get("mission", {}).get("cruiseSpeed", 0),
247
+ "firmware_type": mission_data.get("mission", {}).get("firmwareType", 0),
248
+ "total_items": len(mission_items)
249
+ }
250
+ }
251
+
252
+ except json.JSONDecodeError as e:
253
+ return {
254
+ "status": "error",
255
+ "message": f"Invalid mission file format: {str(e)}",
256
+ "waypoints": []
257
+ }
258
+ except Exception as e:
259
+ return {
260
+ "status": "error",
261
+ "message": f"Failed to retrieve waypoints: {str(e)}",
262
+ "waypoints": []
263
+ }
264
+
265
+
266
  @router.post("/flight/confirm")
267
  async def confirm_flight(mission_data: dict):
268
  """Confirm and start tracking a flight mission.
269
+
270
  Expects JSON with:
271
  - mission_id: str
272
  - drone_id: str
273
+ - waypoints: list of {"lat": float, "lng": float} (user-edited coordinates)
274
+ OR {"lat": float, "lon": float, "alt": float} (original format)
275
+ - altitude: float (optional, defaults to 70m)
276
  """
277
  try:
278
+ # Handle both formats: {lat, lng} and {lat, lon}
279
+ waypoints_data = mission_data.get('waypoints', [])
280
+
281
+ # Normalize waypoints to internal format
282
+ normalized_waypoints = []
283
+ for wp in waypoints_data:
284
+ # Support both {lat, lng} (user-edited) and {lat, lon} (original) formats
285
+ if 'lng' in wp:
286
+ # User-edited format from map
287
+ normalized_waypoints.append({
288
+ 'lat': wp['lat'], # Keep exact precision
289
+ 'lon': wp['lng'], # Note: using lng as longitude
290
+ 'alt': wp.get('alt', mission_data.get('altitude', 70.0))
291
+ })
292
+ elif 'lon' in wp:
293
+ # Original format
294
+ normalized_waypoints.append({
295
+ 'lat': wp['lat'], # Keep exact precision
296
+ 'lon': wp['lon'], # Keep exact precision
297
+ 'alt': wp.get('alt', mission_data.get('altitude', 70.0))
298
+ })
299
+ else:
300
+ # Minimal format, add default altitude
301
+ normalized_waypoints.append({
302
+ 'lat': wp['lat'],
303
+ 'lon': wp.get('lng', wp.get('lon', 0)),
304
+ 'alt': mission_data.get('altitude', 70.0)
305
+ })
306
+
307
+ # Update mission data with normalized waypoints
308
+ mission_data['waypoints'] = normalized_waypoints
309
+
310
  result = await flight_manager.confirm_flight(mission_data)
311
  return result
312
  except Exception as e:
src/core/ai.py CHANGED
@@ -926,7 +926,7 @@ Respond ONLY with valid JSON."""
926
  user_msg = message.get('user', '')
927
  coords = self._extract_coordinates(user_msg)
928
  if coords:
929
- return f"{coords[0]:.6f}, {coords[1]:.6f}"
930
  return "not specified"
931
 
932
  async def _get_coordinates_from_context(self, message: str, chat_history: List[Dict]) -> Optional[List[float]]:
 
926
  user_msg = message.get('user', '')
927
  coords = self._extract_coordinates(user_msg)
928
  if coords:
929
+ return f"{coords[0]}, {coords[1]}"
930
  return "not specified"
931
 
932
  async def _get_coordinates_from_context(self, message: str, chat_history: List[Dict]) -> Optional[List[float]]:
src/core/geocoding.py CHANGED
@@ -71,7 +71,7 @@ class GeocodingService:
71
 
72
  # Validate coordinates and return with 6 decimal precision
73
  if -90 <= lat <= 90 and -180 <= lng <= 180:
74
- return (round(lat, 6), round(lng, 6))
75
  return None
76
  except Exception as e:
77
  print(f"Google geocoding error: {e}")
@@ -103,7 +103,7 @@ class GeocodingService:
103
  lng = location.get('lng')
104
 
105
  if lat is not None and lng is not None:
106
- return (round(float(lat), 6), round(float(lng), 6))
107
 
108
  return None
109
  except Exception as e:
@@ -260,7 +260,7 @@ class GeocodingService:
260
  lat = location.get('lat')
261
  lng = location.get('lng')
262
  if lat is not None and lng is not None:
263
- return (round(float(lat), 6), round(float(lng), 6))
264
  return None
265
  except Exception:
266
  return None
 
71
 
72
  # Validate coordinates and return with 6 decimal precision
73
  if -90 <= lat <= 90 and -180 <= lng <= 180:
74
+ return (float(lat), float(lng))
75
  return None
76
  except Exception as e:
77
  print(f"Google geocoding error: {e}")
 
103
  lng = location.get('lng')
104
 
105
  if lat is not None and lng is not None:
106
+ return (float(lat), float(lng))
107
 
108
  return None
109
  except Exception as e:
 
260
  lat = location.get('lat')
261
  lng = location.get('lng')
262
  if lat is not None and lng is not None:
263
+ return (float(lat), float(lng))
264
  return None
265
  except Exception:
266
  return None
src/services/drone_agent.py CHANGED
@@ -1,4 +1,6 @@
1
  """Main DroneAgent service."""
 
 
2
  from datetime import datetime
3
  from typing import Dict, Any
4
 
@@ -7,9 +9,11 @@ from .mission_planner import MissionPlanner
7
 
8
  class DroneAgent:
9
  """Complete AI system with all features integrated."""
10
-
11
  def __init__(self):
12
  self.mission_planner = MissionPlanner()
 
 
13
 
14
  async def process_message(self, message: str) -> Dict[str, Any]:
15
  """Process user message with conversational AI system."""
@@ -50,6 +54,8 @@ class DroneAgent:
50
  {'type': 'mission_generated', 'filename': filename}
51
  )
52
 
 
 
53
  return response
54
 
55
  except ValueError as e:
@@ -121,7 +127,7 @@ class DroneAgent:
121
  "Mission uses perfect coordinate frames (eliminates takeoff errors)",
122
  f"Altitude optimized for {analysis.mission_type.value} operations",
123
  "Complete landing sequence with DO_LAND_START included",
124
- "QGroundControl import guaranteed to work",
125
  f"Generated by {analysis.model_used} AI model"
126
  ],
127
  "technical_notes": [
 
1
  """Main DroneAgent service."""
2
+ import os
3
+ import asyncio
4
  from datetime import datetime
5
  from typing import Dict, Any
6
 
 
9
 
10
  class DroneAgent:
11
  """Complete AI system with all features integrated."""
12
+
13
  def __init__(self):
14
  self.mission_planner = MissionPlanner()
15
+
16
+
17
 
18
  async def process_message(self, message: str) -> Dict[str, Any]:
19
  """Process user message with conversational AI system."""
 
54
  {'type': 'mission_generated', 'filename': filename}
55
  )
56
 
57
+ # QGC auto-import functionality removed as requested
58
+
59
  return response
60
 
61
  except ValueError as e:
 
127
  "Mission uses perfect coordinate frames (eliminates takeoff errors)",
128
  f"Altitude optimized for {analysis.mission_type.value} operations",
129
  "Complete landing sequence with DO_LAND_START included",
130
+ "Mission file ready for manual import into QGroundControl",
131
  f"Generated by {analysis.model_used} AI model"
132
  ],
133
  "technical_notes": [
src/services/mission_planner.py CHANGED
@@ -148,9 +148,13 @@ DRONE STATUS:
148
  altitude = analysis.altitude
149
  mission_type = analysis.mission_type
150
 
151
- # Generate waypoints
152
  waypoints = self._generate_waypoints(lat, lon, altitude, mission_type, analysis)
153
-
 
 
 
 
154
  # Create QGC mission structure
155
  mission_items = self._create_mission_items(lat, lon, altitude, waypoints)
156
 
@@ -273,3 +277,35 @@ DRONE STATUS:
273
  })
274
 
275
  return mission_items
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  altitude = analysis.altitude
149
  mission_type = analysis.mission_type
150
 
151
+ # Generate waypoints with straight-line prioritization during development
152
  waypoints = self._generate_waypoints(lat, lon, altitude, mission_type, analysis)
153
+
154
+ # DEVELOPMENT MODE: Prioritize straight-line plans for validation
155
+ if self._is_development_mode():
156
+ waypoints = self._prioritize_straight_line_waypoints(lat, lon, altitude, mission_type, analysis, waypoints)
157
+
158
  # Create QGC mission structure
159
  mission_items = self._create_mission_items(lat, lon, altitude, waypoints)
160
 
 
277
  })
278
 
279
  return mission_items
280
+
281
+ def _is_development_mode(self) -> bool:
282
+ """Check if we're in development mode for straight-line prioritization."""
283
+ import os
284
+ return os.getenv('DRONE_AGENT_DEV_MODE', 'true').lower() == 'true'
285
+
286
+ def _prioritize_straight_line_waypoints(self, lat: float, lon: float, altitude: int,
287
+ mission_type: MissionType, analysis: MissionAnalysis,
288
+ original_waypoints: list) -> list:
289
+ """Prioritize straight-line waypoints during development for easier validation."""
290
+ print(f"🔧 DEVELOPMENT MODE: Prioritizing straight-line waypoints for {mission_type.value}")
291
+
292
+ # For development, create simple straight-line waypoints regardless of mission type
293
+ # This helps validate the coordinate editing and confirmation workflow
294
+
295
+ if mission_type == MissionType.GO_STRAIGHT:
296
+ # Keep original straight-line waypoints but ensure exact precision
297
+ return original_waypoints
298
+ else:
299
+ # For other mission types, also create straight-line during development
300
+ # This ensures consistent behavior for testing coordinate editing
301
+ direction = analysis.direction or 'north'
302
+ distance = max(analysis.distance or 100, 50) # Minimum 50m for visibility
303
+
304
+ straight_waypoints = self.knowledge_base.create_straight_line_waypoints(
305
+ lat, lon, direction, distance, 3 # Always 3 waypoints for simplicity
306
+ )
307
+
308
+ print(f"🔧 DEV MODE: Created {len(straight_waypoints)} straight-line waypoints")
309
+ print(f"🔧 DEV MODE: Direction: {direction}, Distance: {distance}m")
310
+
311
+ return straight_waypoints
waypoint_editor.html ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🚁 DroneAgent - Waypoint Editor</title>
7
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
8
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
9
+ <style>
10
+ body {
11
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
12
+ margin: 0;
13
+ padding: 0;
14
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
15
+ color: white;
16
+ }
17
+
18
+ .container {
19
+ display: flex;
20
+ height: 100vh;
21
+ }
22
+
23
+ .sidebar {
24
+ width: 350px;
25
+ background: rgba(255, 255, 255, 0.1);
26
+ backdrop-filter: blur(10px);
27
+ padding: 20px;
28
+ overflow-y: auto;
29
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
30
+ }
31
+
32
+ .map-container {
33
+ flex: 1;
34
+ position: relative;
35
+ }
36
+
37
+ #map {
38
+ height: 100%;
39
+ width: 100%;
40
+ }
41
+
42
+ .header {
43
+ background: rgba(255, 255, 255, 0.2);
44
+ padding: 15px;
45
+ border-radius: 10px;
46
+ margin-bottom: 20px;
47
+ text-align: center;
48
+ }
49
+
50
+ .waypoint-item {
51
+ background: rgba(255, 255, 255, 0.1);
52
+ padding: 10px;
53
+ margin: 8px 0;
54
+ border-radius: 8px;
55
+ border-left: 4px solid #4CAF50;
56
+ cursor: pointer;
57
+ transition: all 0.3s ease;
58
+ }
59
+
60
+ .waypoint-item:hover {
61
+ background: rgba(255, 255, 255, 0.2);
62
+ transform: translateX(5px);
63
+ }
64
+
65
+ .waypoint-item.active {
66
+ background: rgba(76, 175, 80, 0.3);
67
+ border-left-color: #4CAF50;
68
+ }
69
+
70
+ .btn {
71
+ background: linear-gradient(45deg, #4CAF50, #45a049);
72
+ color: white;
73
+ border: none;
74
+ padding: 12px 20px;
75
+ border-radius: 8px;
76
+ cursor: pointer;
77
+ font-size: 14px;
78
+ font-weight: bold;
79
+ transition: all 0.3s ease;
80
+ width: 100%;
81
+ margin: 8px 0;
82
+ }
83
+
84
+ .btn:hover {
85
+ transform: translateY(-2px);
86
+ box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3);
87
+ }
88
+
89
+ .btn:disabled {
90
+ background: #666;
91
+ cursor: not-allowed;
92
+ transform: none;
93
+ }
94
+
95
+ .status {
96
+ padding: 10px;
97
+ border-radius: 8px;
98
+ margin: 10px 0;
99
+ font-weight: bold;
100
+ }
101
+
102
+ .status.success {
103
+ background: rgba(76, 175, 80, 0.2);
104
+ border-left: 4px solid #4CAF50;
105
+ }
106
+
107
+ .status.error {
108
+ background: rgba(244, 67, 54, 0.2);
109
+ border-left: 4px solid #f44336;
110
+ }
111
+
112
+ .status.info {
113
+ background: rgba(33, 150, 243, 0.2);
114
+ border-left: 4px solid #2196F3;
115
+ }
116
+
117
+ .coordinate-display {
118
+ font-family: 'Courier New', monospace;
119
+ background: rgba(0, 0, 0, 0.2);
120
+ padding: 8px;
121
+ border-radius: 4px;
122
+ margin: 5px 0;
123
+ font-size: 12px;
124
+ }
125
+
126
+ .loading {
127
+ display: inline-block;
128
+ width: 20px;
129
+ height: 20px;
130
+ border: 3px solid rgba(255, 255, 255, 0.3);
131
+ border-radius: 50%;
132
+ border-top-color: white;
133
+ animation: spin 1s ease-in-out infinite;
134
+ }
135
+
136
+ @keyframes spin {
137
+ to { transform: rotate(360deg); }
138
+ }
139
+ </style>
140
+ </head>
141
+ <body>
142
+ <div class="container">
143
+ <div class="sidebar">
144
+ <div class="header">
145
+ <h2>🚁 Waypoint Editor</h2>
146
+ <p>Edit mission waypoints on the map</p>
147
+ </div>
148
+
149
+ <div id="status" class="status info">
150
+ Click "Load Mission" to get started
151
+ </div>
152
+
153
+ <button id="loadBtn" class="btn">📥 Load Mission Waypoints</button>
154
+ <button id="saveBtn" class="btn" disabled>💾 Confirm Flight with Edited Waypoints</button>
155
+ <button id="resetBtn" class="btn" disabled>🔄 Reset Changes</button>
156
+
157
+ <div id="waypointsList">
158
+ <!-- Waypoints will be loaded here -->
159
+ </div>
160
+
161
+ <div id="coordinatesDisplay" style="margin-top: 20px;">
162
+ <!-- Coordinate details will be shown here -->
163
+ </div>
164
+ </div>
165
+
166
+ <div class="map-container">
167
+ <div id="map"></div>
168
+ </div>
169
+ </div>
170
+
171
+ <script>
172
+ // Initialize map
173
+ const map = L.map('map').setView([16.047, 108.206], 15);
174
+
175
+ // Add OpenStreetMap tiles
176
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
177
+ attribution: '© OpenStreetMap contributors'
178
+ }).addTo(map);
179
+
180
+ // Store waypoints and markers
181
+ let waypoints = [];
182
+ let markers = [];
183
+ let originalWaypoints = [];
184
+ let selectedWaypointIndex = -1;
185
+
186
+ // API functions
187
+ async function loadWaypoints() {
188
+ try {
189
+ updateStatus('Loading waypoints...', 'info');
190
+ showLoading(true, 'loadBtn');
191
+
192
+ const response = await fetch('/mission/waypoints');
193
+ const data = await response.json();
194
+
195
+ if (data.status === 'success') {
196
+ waypoints = data.waypoints;
197
+ originalWaypoints = JSON.parse(JSON.stringify(waypoints)); // Deep copy
198
+
199
+ displayWaypoints();
200
+ updateMapMarkers();
201
+ updateStatus(`✅ Loaded ${waypoints.length} waypoints`, 'success');
202
+
203
+ document.getElementById('saveBtn').disabled = false;
204
+ document.getElementById('resetBtn').disabled = false;
205
+ } else {
206
+ updateStatus(`❌ ${data.message}`, 'error');
207
+ }
208
+ } catch (error) {
209
+ updateStatus(`❌ Error loading waypoints: ${error.message}`, 'error');
210
+ } finally {
211
+ showLoading(false, 'loadBtn');
212
+ }
213
+ }
214
+
215
+ async function confirmFlight() {
216
+ try {
217
+ updateStatus('Confirming flight...', 'info');
218
+ showLoading(true, 'saveBtn');
219
+
220
+ const confirmData = {
221
+ mission_id: `edited_mission_${Date.now()}`,
222
+ drone_id: 'drone_001',
223
+ waypoints: waypoints,
224
+ altitude: 70.0
225
+ };
226
+
227
+ const response = await fetch('/flight/confirm', {
228
+ method: 'POST',
229
+ headers: {
230
+ 'Content-Type': 'application/json',
231
+ },
232
+ body: JSON.stringify(confirmData)
233
+ });
234
+
235
+ const data = await response.json();
236
+
237
+ if (data.status === 'confirmed' || response.ok) {
238
+ updateStatus('✅ Flight confirmed with edited waypoints!', 'success');
239
+ alert('🎉 Flight confirmed successfully!\n\nYour edited waypoints have been saved and the flight mission is ready.');
240
+ } else {
241
+ updateStatus(`❌ Confirmation failed: ${data.message}`, 'error');
242
+ }
243
+ } catch (error) {
244
+ updateStatus(`❌ Error confirming flight: ${error.message}`, 'error');
245
+ } finally {
246
+ showLoading(false, 'saveBtn');
247
+ }
248
+ }
249
+
250
+ function resetChanges() {
251
+ waypoints = JSON.parse(JSON.stringify(originalWaypoints));
252
+ displayWaypoints();
253
+ updateMapMarkers();
254
+ updateStatus('🔄 Changes reset to original waypoints', 'info');
255
+ }
256
+
257
+ function displayWaypoints() {
258
+ const list = document.getElementById('waypointsList');
259
+ list.innerHTML = '<h3>📍 Waypoints</h3>';
260
+
261
+ waypoints.forEach((wp, index) => {
262
+ const item = document.createElement('div');
263
+ item.className = 'waypoint-item';
264
+ item.onclick = () => selectWaypoint(index);
265
+
266
+ const latDiff = originalWaypoints[index] ?
267
+ (wp.lat - originalWaypoints[index].lat).toFixed(6) : '0.000000';
268
+ const lngDiff = originalWaypoints[index] ?
269
+ (wp.lng - originalWaypoints[index].lng).toFixed(6) : '0.000000';
270
+
271
+ const hasChanged = Math.abs(latDiff) > 0.000001 || Math.abs(lngDiff) > 0.000001;
272
+
273
+ item.innerHTML = `
274
+ <strong>Waypoint ${index + 1}</strong><br>
275
+ <div class="coordinate-display">
276
+ Lat: ${wp.lat.toFixed(8)}<br>
277
+ Lng: ${wp.lng.toFixed(8)}
278
+ </div>
279
+ ${hasChanged ? '<small style="color: #FFC107;">✏️ Modified</small>' : '<small style="color: #4CAF50;">✅ Original</small>'}
280
+ `;
281
+
282
+ list.appendChild(item);
283
+ });
284
+ }
285
+
286
+ function selectWaypoint(index) {
287
+ selectedWaypointIndex = index;
288
+
289
+ // Update UI
290
+ document.querySelectorAll('.waypoint-item').forEach((item, i) => {
291
+ item.classList.toggle('active', i === index);
292
+ });
293
+
294
+ // Pan map to selected waypoint
295
+ if (waypoints[index]) {
296
+ map.setView([waypoints[index].lat, waypoints[index].lng], 18);
297
+ }
298
+
299
+ updateCoordinateDisplay();
300
+ }
301
+
302
+ function updateCoordinateDisplay() {
303
+ const display = document.getElementById('coordinatesDisplay');
304
+
305
+ if (selectedWaypointIndex >= 0 && waypoints[selectedWaypointIndex]) {
306
+ const wp = waypoints[selectedWaypointIndex];
307
+ const original = originalWaypoints[selectedWaypointIndex];
308
+
309
+ display.innerHTML = `
310
+ <h4>🎯 Selected Waypoint ${selectedWaypointIndex + 1}</h4>
311
+ <div class="coordinate-display">
312
+ <strong>Current:</strong><br>
313
+ Latitude: ${wp.lat}<br>
314
+ Longitude: ${wp.lng}
315
+ </div>
316
+ ${original ? `
317
+ <div class="coordinate-display">
318
+ <strong>Original:</strong><br>
319
+ Latitude: ${original.lat}<br>
320
+ Longitude: ${original.lng}
321
+ </div>
322
+ <div class="coordinate-display">
323
+ <strong>Difference:</strong><br>
324
+ Lat: ${(wp.lat - original.lat).toFixed(8)}<br>
325
+ Lng: ${(wp.lng - original.lng).toFixed(8)}
326
+ </div>
327
+ ` : ''}
328
+ `;
329
+ } else {
330
+ display.innerHTML = '<p style="color: #999;">Click a waypoint to see details</p>';
331
+ }
332
+ }
333
+
334
+ function updateMapMarkers() {
335
+ // Clear existing markers
336
+ markers.forEach(marker => map.removeLayer(marker));
337
+ markers = [];
338
+
339
+ // Add new markers
340
+ waypoints.forEach((wp, index) => {
341
+ const marker = L.marker([wp.lat, wp.lng], {
342
+ draggable: true,
343
+ title: `Waypoint ${index + 1}`
344
+ });
345
+
346
+ // Create custom icon with waypoint number
347
+ const customIcon = L.divIcon({
348
+ className: 'waypoint-marker',
349
+ html: `<div style="
350
+ background: #4CAF50;
351
+ color: white;
352
+ border-radius: 50%;
353
+ width: 30px;
354
+ height: 30px;
355
+ display: flex;
356
+ align-items: center;
357
+ justify-content: center;
358
+ font-weight: bold;
359
+ border: 2px solid white;
360
+ box-shadow: 0 2px 5px rgba(0,0,0,0.3);
361
+ ">${index + 1}</div>`,
362
+ iconSize: [30, 30],
363
+ iconAnchor: [15, 15]
364
+ });
365
+
366
+ marker.setIcon(customIcon);
367
+
368
+ // Handle drag events
369
+ marker.on('dragend', function(event) {
370
+ const newPos = event.target.getLatLng();
371
+ waypoints[index] = {
372
+ lat: newPos.lat,
373
+ lng: newPos.lng
374
+ };
375
+ displayWaypoints();
376
+ updateCoordinateDisplay();
377
+ updateStatus('📍 Waypoint moved - click Save to confirm', 'info');
378
+ });
379
+
380
+ marker.addTo(map);
381
+ markers.push(marker);
382
+ });
383
+
384
+ // Fit map to show all waypoints
385
+ if (waypoints.length > 0) {
386
+ const bounds = L.latLngBounds(waypoints.map(wp => [wp.lat, wp.lng]));
387
+ map.fitBounds(bounds, { padding: [20, 20] });
388
+ }
389
+ }
390
+
391
+ function updateStatus(message, type) {
392
+ const status = document.getElementById('status');
393
+ status.textContent = message;
394
+ status.className = `status ${type}`;
395
+ }
396
+
397
+ function showLoading(show, buttonId) {
398
+ const button = document.getElementById(buttonId);
399
+ const loading = button.querySelector('.loading');
400
+
401
+ if (show) {
402
+ if (!loading) {
403
+ button.innerHTML = '<div class="loading"></div> Loading...';
404
+ }
405
+ button.disabled = true;
406
+ } else {
407
+ if (loading) {
408
+ button.innerHTML = button.innerHTML.replace('<div class="loading"></div> Loading...', button.textContent);
409
+ }
410
+ button.disabled = false;
411
+ }
412
+ }
413
+
414
+ // Event listeners
415
+ document.getElementById('loadBtn').addEventListener('click', loadWaypoints);
416
+ document.getElementById('saveBtn').addEventListener('click', confirmFlight);
417
+ document.getElementById('resetBtn').addEventListener('click', resetChanges);
418
+
419
+ // Initialize
420
+ updateStatus('🚁 Welcome to Waypoint Editor! Click "Load Mission" to get started.', 'info');
421
+ </script>
422
+ </body>
423
+ </html>