diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..1d4668a54 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Code owners for the entire repository +* @kungfuchicken @prakhyatpandit @BohanZhang1 diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml new file mode 100644 index 000000000..890ea6cbf --- /dev/null +++ b/.github/workflows/backend-ci.yml @@ -0,0 +1,55 @@ +name: Backend CI + +on: + push: + branches: + - main + - docker-main + pull_request: + branches: + - main + - docker-main + +jobs: + backend-setup: + runs-on: windows-latest + strategy: + matrix: + python-version: [3.10] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}- + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r backend/requirements.txt + + - name: Lint backend code + run: | + pip install flake8 + flake8 backend/ --max-line-length 160 --ignore E127,E251,E302,E501,E261,E262,E265,F401,F403,F405,F841,W293,E128,E303,E305,W292,E122,W391,E111,E117,E301,E125,E222,E226,E201,E203,E202,E721,W503,W504,E272,E126,E225,W291,E211,E231,E722,W191,E712,F523,E402,E306 + + - name: Format backend code with Black + run: | + pip install black + black backend/ + + - name: Perform Dependency Audit + run: | + pip install safety + safety check -r backend/requirements.txt \ No newline at end of file diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml new file mode 100644 index 000000000..bb5c22222 --- /dev/null +++ b/.github/workflows/frontend-ci.yml @@ -0,0 +1,57 @@ +name: Frontend CI + +on: + push: + branches: + - main + - docker-main + pull_request: + branches: + - main + - docker-main + +jobs: + frontend-setup: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Node.js modules + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '16' + + - name: Install dependencies + run: | + cd frontend + npm install + + - name: Lint frontend code + run: | + cd frontend + npm run lint + + - name: Format frontend code with Prettier + run: | + cd frontend + npm run format + + - name: Build frontend + run: | + cd frontend + npm run build + + - name: Run ESLint Security Checks + run: | + cd frontend + npm audit --omit=dev --audit-level=critical || true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..cad849b16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# Ignore .env files +.env + +# Ignore Visual Studio files +.vs/ + +# Ignore virtual environments +backend/venv/ + +# Ignore Node.js dependencies +node_modules/ + +# Ignore Python bytecode files and __pycache__ directories globally +*.pyc +__pycache__/ + +# Ignore backend-specific files +backend/*.csv +backend/*.html +backend/*.png +backend/*.jpg + +# Frontend ignores +# dependencies +frontend/node_modules +frontend/.pnp +frontend/.pnp.js + +# testing +frontend/coverage + +# production +frontend/build + +# misc +frontend/.DS_Store +frontend/.env.local +frontend/.env.development.local +frontend/.env.test.local +frontend/.env.production.local + +frontend/npm-debug.log* +frontend/yarn-debug.log* +frontend/yarn-error.log* + +# Ignore frontend package-lock.json +frontend/package-lock.json + +# Ignore Windows system file +desktop.ini + +# Ignore Google Cloud service account key +key.json + +# Ignore config directory; so that changes wont be made to it +config/ +config/fake-gcs-data/ + +# MaOS system files +.DS_Store + +# VS Code workspace files +*.code-workspace \ No newline at end of file diff --git a/LICENSE b/LICENSE index d12c9c762..1e03cbea2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Safe Aerial Systems Lab +Copyright (c) 2024 Open Source with SLU Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 000000000..cfae6f802 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,16 @@ +# Ignore Python virtual environments and cache files +__pycache__/ +*.pyc +*.pyo + +# Ignore local logs and temporary files +*.log +tmp/ +*.tmp + +# Ignore OS-generated files +.DS_Store +Thumbs.db + +# Ignore Git repository +.git diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index 6b2bbf5ac..000000000 --- a/backend/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# ignore all csv files -*.csv - -# ignore all html files -*.html - -# ignore all png and jpg files -*.png -*.jpg diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 000000000..8b82c4f71 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,31 @@ +# Use the official Python 3.10 image +FROM python:3.10-slim + +# Install system-level dependencies required for the Python packages +RUN apt-get update && apt-get install -y \ + build-essential \ + libasound-dev \ + libportaudio2 \ + libportaudiocpp0 \ + portaudio19-dev \ + libjpeg-dev \ + zlib1g-dev \ + # libglx-mesa0 \ + libgl1 \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +# Set the working directory in the container +WORKDIR /app + +# Copy the backend code into the container +COPY . . + +# Install the Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Expose the port the backend will use +EXPOSE 5000 + +# Command to start the backend server +CMD ["flask", "--app", "PythonClient.server.simulation_server", "run", "--host=0.0.0.0", "--port=5000", "--debug"] \ No newline at end of file diff --git a/backend/PythonClient/airsim/client.py b/backend/PythonClient/airsim/client.py index bbc78eb13..7127e1e5c 100644 --- a/backend/PythonClient/airsim/client.py +++ b/backend/PythonClient/airsim/client.py @@ -13,7 +13,7 @@ class VehicleClient: def __init__(self, ip = "", port = 41451, timeout_value = 3600): if (ip == ""): - ip = "127.0.0.1" + ip = "host.docker.internal" self.client = msgpackrpc.Client(msgpackrpc.Address(ip, port), timeout = timeout_value, pack_encoding = 'utf-8', unpack_encoding = 'utf-8') #----------------------------------- Common vehicle APIs --------------------------------------------- diff --git a/backend/PythonClient/multirotor/airsim_application.py b/backend/PythonClient/multirotor/airsim_application.py index 6c02ec620..65f3bdef7 100644 --- a/backend/PythonClient/multirotor/airsim_application.py +++ b/backend/PythonClient/multirotor/airsim_application.py @@ -5,11 +5,14 @@ from abc import abstractmethod from PythonClient import airsim - +from PythonClient.multirotor.storage.storage_config import get_storage_service class AirSimApplication: # Parent class for all airsim client side mission and monitors def __init__(self): + # Set up the storage service + self.storage_service = get_storage_service() + self.circular_mission_names = {"FlyInCircle"} self.polygon_mission_names = {"FlyToPoints", "FlyToPointsGeo"} self.point_mission_names = {"FlyStraight"} @@ -59,6 +62,13 @@ def append_pass_to_log(self, new_log_string): def save_report(self): pass + def save_report_to_storage(self, file_name, content, content_type='text/plain'): + """ + Saves the content as a report and uploads it using the storage service. + Uses the upload_to_service method of the storage service. + """ + self.storage_service.upload_to_service(file_name, content, content_type) + def save_pic(self, picture): self.snap_shots.append(picture) @@ -104,4 +114,4 @@ def get_cesium_origin(self): lat = data["latitude"] lon = data["longitude"] height = data["height"] - return [lat, lon, height] + return [lat, lon, height] \ No newline at end of file diff --git a/backend/PythonClient/multirotor/mission/abstract/abstract_mission.py b/backend/PythonClient/multirotor/mission/abstract/abstract_mission.py index be55c6a38..751ba3fec 100644 --- a/backend/PythonClient/multirotor/mission/abstract/abstract_mission.py +++ b/backend/PythonClient/multirotor/mission/abstract/abstract_mission.py @@ -2,10 +2,8 @@ import os import threading from enum import Enum - from PythonClient.multirotor.airsim_application import AirSimApplication - lock = threading.Lock() @@ -34,24 +32,17 @@ def async_fly_to_position(self, drone_name, point, speed): def save_report(self): with lock: - log_dir = os.path.join(self.dir_path, self.log_subdir, self.__class__.__name__) - # print("DEBUG:" + log_dir) - if not os.path.exists(log_dir): - try: - os.makedirs(log_dir) - except: - print("Folder exist, thread unsafe") + # Directly create the file name for GCS + file_name = self.__class__.__name__ + "_" + self.target_drone + "_log.txt" + gcs_path = f"{self.log_subdir}/{self.__class__.__name__}/{file_name}" - with open(log_dir + os.sep + self.__class__.__name__ + "_" + self.target_drone + "_log.txt", 'w') as outfile: - outfile.write(self.log_text) + # Upload directly to GCS (log_text is uploaded as file content) + self.save_report_to_storage(gcs_path, self.log_text) def kill_mission(self): self.state = self.State.END # kill all threads - - - if __name__ == '__main__': - mission = GenericMission() + mission = GenericMission() \ No newline at end of file diff --git a/backend/PythonClient/multirotor/monitor/abstract/globa_monitor.py b/backend/PythonClient/multirotor/monitor/abstract/globa_monitor.py index f2a19f9f4..cb5464dfa 100644 --- a/backend/PythonClient/multirotor/monitor/abstract/globa_monitor.py +++ b/backend/PythonClient/multirotor/monitor/abstract/globa_monitor.py @@ -28,14 +28,11 @@ async def send_notification(websocket, path): def save_report(self): with lock: - log_dir = os.path.join(self.dir_path, - self.log_subdir) + os.sep + "GlobalMonitors" + os.sep + self.__class__.__name__ - if not os.path.exists(log_dir): - os.makedirs(log_dir) + # Directly create the file name for GCS + file_name = "log.txt" + gcs_path = f"{self.log_subdir}/GlobalMonitors/{self.__class__.__name__}/{file_name}" - filename = log_dir + os.sep + "log.txt" + # Upload directly to GCS (log_text is uploaded as file content) + self.save_report_to_storage(gcs_path, self.log_text) - with open(filename, 'w') as outfile: - outfile.write(self.log_text) - - # print("DEBUG:" + log_dir) + # print("DEBUG:" + log_dir) \ No newline at end of file diff --git a/backend/PythonClient/multirotor/monitor/abstract/single_drone_mission_monitor.py b/backend/PythonClient/multirotor/monitor/abstract/single_drone_mission_monitor.py index 1dfc66449..8b063f85a 100644 --- a/backend/PythonClient/multirotor/monitor/abstract/single_drone_mission_monitor.py +++ b/backend/PythonClient/multirotor/monitor/abstract/single_drone_mission_monitor.py @@ -37,14 +37,9 @@ def get_graph_dir(self): def save_report(self): with lock: - log_dir = os.path.join(self.dir_path, - self.log_subdir) + os.sep + self.mission.__class__.__name__ + os.sep + self.__class__.__name__ - if not os.path.exists(log_dir): - os.makedirs(log_dir) + # Directly create the file name for GCS + file_name = self.mission.target_drone + "_log.txt" + gcs_path = f"{self.log_subdir}/{self.mission.__class__.__name__}/{self.__class__.__name__}/{file_name}" - filename = log_dir + os.sep + self.mission.target_drone + "_log.txt" - - with open(filename, 'w') as outfile: - outfile.write(self.log_text) - - # print("DEBUG:" + log_dir) + # Upload directly to GCS (log_text is uploaded as file content) + self.save_report_to_storage(gcs_path, self.log_text) \ No newline at end of file diff --git a/backend/PythonClient/multirotor/monitor/circular_deviation_monitor.py b/backend/PythonClient/multirotor/monitor/circular_deviation_monitor.py index ac173e5f1..78728dad0 100644 --- a/backend/PythonClient/multirotor/monitor/circular_deviation_monitor.py +++ b/backend/PythonClient/multirotor/monitor/circular_deviation_monitor.py @@ -94,7 +94,7 @@ def update_position(self): if not self.reported_breach: self.reported_breach = True self.append_fail_to_log(f"{self.target_drone};First breach: deviated more than " - f"{self.deviation_percentage}meter from the planned route") + f"{self.deviation_percentage} meter from the planned route") self.breach_flag = True self.est_position_array.append([x, y, z]) self.obj_position_array.append([ox, oy, oz]) @@ -103,12 +103,12 @@ def update_position(self): # print(self.position_array) def check_breach(self, x, y, z): - #print(f"Checking breach, Current position: {round(x, 2)}, {round(y, 2)}, {round(z, 2)}, " + # print(f"Checking breach, Current position: {round(x, 2)}, {round(y, 2)}, {round(z, 2)}, " # f"center: {self.mission.center.x_val}, {self.mission.center.y_val}, {self.mission.altitude}") return GeoUtil.is_point_close_to_circle([self.mission.center.x_val, self.mission.center.y_val, self.mission.altitude], - self.mission.radius, - [x, y, -z], - self.deviation_percentage) + self.mission.radius, + [x, y, -z], + self.deviation_percentage) def calculate_actual_distance(self): distance = 0.0 @@ -118,36 +118,40 @@ def calculate_actual_distance(self): @staticmethod def get_distance_btw_points(point_arr_1, point_arr_2): - return math.sqrt((point_arr_2[0] - point_arr_1[0]) ** 2 + (point_arr_2[1] - point_arr_1[1]) ** 2 + ( - point_arr_2[2] - point_arr_1[2]) ** 2) + return math.sqrt((point_arr_2[0] - point_arr_1[0]) ** 2 + + (point_arr_2[1] - point_arr_1[1]) ** 2 + + (point_arr_2[2] - point_arr_1[2]) ** 2) def draw_trace_3d(self): - graph_dir = self.get_graph_dir() + # Construct folder path + folder_path = f"{self.log_subdir}/{self.mission.__class__.__name__}/{self.__class__.__name__}/" est_actual = self.est_position_array # obj_actual = self.obj_position_array radius = self.mission.radius height = self.mission.altitude + if not self.breach_flag: title = f"{self.target_drone} Planned vs. Actual\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}" else: title = f"(FAILED) {self.target_drone} Planned vs. Actual\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}" + center = [self.mission.center.x_val, self.mission.center.y_val, height] theta = np.linspace(0, 2 * np.pi, 100) x = center[0] + radius * np.cos(theta) y = center[1] + radius * np.sin(theta) z = np.ones(100) * height - planned = [] - for i in range(len(x)): - planned.append([x[i], y[i], -z[i]]) - - ThreeDimensionalGrapher.draw_trace_vs_planned(planed_position_list=planned, - actual_position_list=est_actual, - full_target_directory=graph_dir, - drone_name=self.target_drone, - title=title) - ThreeDimensionalGrapher.draw_interactive_trace_vs_planned(planed_position_list=planned, - actual_position_list=est_actual, - full_target_directory=graph_dir, - drone_name=self.target_drone, - title=title) + planned = [[x[i], y[i], -z[i]] for i in range(len(x))] + + # Use the grapher to draw and upload graphs + grapher = ThreeDimensionalGrapher(self.storage_service) + grapher.draw_trace_vs_planned(planed_position_list=planned, + actual_position_list=est_actual, + drone_name=self.target_drone, + title=title, + folder_path=folder_path) + grapher.draw_interactive_trace_vs_planned(planed_position_list=planned, + actual_position_list=est_actual, + drone_name=self.target_drone, + title=title, + folder_path=folder_path) diff --git a/backend/PythonClient/multirotor/monitor/drift_monitor.py b/backend/PythonClient/multirotor/monitor/drift_monitor.py index a0c713dc8..37eca548e 100644 --- a/backend/PythonClient/multirotor/monitor/drift_monitor.py +++ b/backend/PythonClient/multirotor/monitor/drift_monitor.py @@ -29,7 +29,7 @@ def start(self): def make_drifted_array(self): dt = self.dt - closest = 9223372036854775807 # Max int + closest = float('inf') # Maximum distance self.reached = False self.est_position_array = [] while self.mission.state != self.mission.State.END: @@ -54,26 +54,32 @@ def make_drifted_array(self): f"{round(self.threshold, 2)} meters. Closest distance: {round(closest, 2)} meters") def draw_trace_3d(self): + # Construct folder path for storage service + folder_path = f"{self.log_subdir}/{self.mission.__class__.__name__}/{self.__class__.__name__}/" + actual = self.est_position_array dest = self.mission.point + + # Determine the title based on whether the target was reached if self.reached: title = f"Drift path\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}\n" \ - f"Closest distance: {round(self.closest,2)} meters" + f"Closest distance: {round(self.closest, 2)} meters" else: title = f"(FAILED) Drift path\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}\n" \ - f"Closest distance: {round(self.closest,2)} meters" - graph_dir = self.get_graph_dir() - grapher = ThreeDimensionalGrapher() + f"Closest distance: {round(self.closest, 2)} meters" + + # Use the grapher to draw and upload graphs + grapher = ThreeDimensionalGrapher(self.storage_service) grapher.draw_trace_vs_point(destination_point=dest, actual_position_list=actual, - full_target_directory=graph_dir, drone_name=self.target_drone, - title=title) + title=title, + folder_path=folder_path) grapher.draw_interactive_trace_vs_point(actual_position=actual, destination=dest, - full_target_directory=graph_dir, drone_name=self.target_drone, - title=title) + title=title, + folder_path=folder_path) if __name__ == "__main__": diff --git a/backend/PythonClient/multirotor/monitor/point_deviation_monitor.py b/backend/PythonClient/multirotor/monitor/point_deviation_monitor.py index 334329e28..b16e05bdf 100644 --- a/backend/PythonClient/multirotor/monitor/point_deviation_monitor.py +++ b/backend/PythonClient/multirotor/monitor/point_deviation_monitor.py @@ -124,22 +124,29 @@ def calculate_actual_distance(self): return distance def draw_trace_3d(self): - graph_dir = self.get_graph_dir() + # Construct the folder path + folder_path = f"{self.log_subdir}/{self.mission.__class__.__name__}/{self.__class__.__name__}/" + + # Ensure the title reflects the mission status if not self.breach_flag: title = f"{self.target_drone} Planned vs. Actual\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}" else: title = f"(FAILED) {self.target_drone} Planned vs. Actual\nDrone speed: {self.mission.speed} m/s\nWind: {self.wind_speed_text}" - grapher = ThreeDimensionalGrapher() - grapher.draw_trace_vs_planned(planed_position_list=self.mission.points, - actual_position_list=self.est_position_array, - full_target_directory=graph_dir, - drone_name=self.target_drone, - title=title - ) - - grapher.draw_interactive_trace_vs_planned(planed_position_list=self.mission.points, - actual_position_list=self.est_position_array, - full_target_directory=graph_dir, - drone_name=self.target_drone, - title=title - ) + + # Draw and upload the graphs + grapher = ThreeDimensionalGrapher(self.storage_service) + grapher.draw_trace_vs_planned( + planed_position_list=self.mission.points, + actual_position_list=self.est_position_array, + drone_name=self.target_drone, + title=title, + folder_path=folder_path + ) + + grapher.draw_interactive_trace_vs_planned( + planed_position_list=self.mission.points, + actual_position_list=self.est_position_array, + drone_name=self.target_drone, + title=title, + folder_path=folder_path + ) \ No newline at end of file diff --git a/backend/PythonClient/multirotor/storage/__init__.py b/backend/PythonClient/multirotor/storage/__init__.py new file mode 100644 index 000000000..06b75c869 --- /dev/null +++ b/backend/PythonClient/multirotor/storage/__init__.py @@ -0,0 +1,3 @@ +from .gcs_storage_service import GCSStorageService +from .gd_storage_service import GoogleDriveStorageService +# Import additional storage services here as they are added diff --git a/backend/PythonClient/multirotor/storage/abstract/storage_service.py b/backend/PythonClient/multirotor/storage/abstract/storage_service.py new file mode 100644 index 000000000..7161d77f0 --- /dev/null +++ b/backend/PythonClient/multirotor/storage/abstract/storage_service.py @@ -0,0 +1,26 @@ +# storage_service.py + +from abc import ABC, abstractmethod + +class StorageServiceInterface(ABC): + """Interface for cloud storage services.""" + + @abstractmethod + def upload_to_service(self, file_name, content, content_type='text/plain'): + """Uploads a file to the cloud storage service.""" + pass + + @abstractmethod + def list_reports(self): + """Lists all report batches from the storage service.""" + pass + + @abstractmethod + def list_folder_contents(self, folder_name): + """Lists the contents of a specific report folder.""" + pass + + @abstractmethod + def serve_html(self, folder_name, relative_path): + """Serves an HTML file from the storage service.""" + pass diff --git a/backend/PythonClient/multirotor/storage/gcs_storage_service.py b/backend/PythonClient/multirotor/storage/gcs_storage_service.py new file mode 100644 index 000000000..e2a2ae9a7 --- /dev/null +++ b/backend/PythonClient/multirotor/storage/gcs_storage_service.py @@ -0,0 +1,224 @@ +from PythonClient.multirotor.storage.abstract.storage_service import StorageServiceInterface +from google.cloud import storage +import base64 +import os +import json + +class GCSStorageService(StorageServiceInterface): + """Concrete class for uploading to GCS.""" + + def __init__(self, bucket_name='your_bucket_name'): + """Initializes the GCS client and bucket.""" + credentials_path = os.getenv('GCS_CREDENTIALS_PATH', 'key.json') + + # Check if using emulator + emulator_host = os.getenv('STORAGE_EMULATOR_HOST') + + if emulator_host: + # For fake-gcs-server, use anonymous credentials + from google.auth.credentials import AnonymousCredentials + self.storage_client = storage.Client( + credentials=AnonymousCredentials(), + project='test-project', + client_options={"api_endpoint": emulator_host} + ) + else: + # Production: use service account + self.storage_client = storage.Client.from_service_account_json(credentials_path) + + self.bucket = self.storage_client.bucket(bucket_name) + + def upload_to_service(self, file_name, content, content_type='text/plain'): + """Uploads a file to the GCS bucket.""" + blob = self.bucket.blob(f'reports/{file_name}') + blob.upload_from_string(content, content_type=content_type) + print(f"File {file_name} uploaded to GCS.") + + def list_reports(self): + """Lists all report batches from GCS.""" + try: + blobs = self.bucket.list_blobs(prefix='reports/') + report_files = [] + + for blob in blobs: + parts = blob.name.split('/') + if len(parts) < 2: + continue + batch_folder = parts[1] + if batch_folder not in [report['filename'] for report in report_files]: + report_files.append({ + 'filename': batch_folder, + 'contains_fuzzy': False, + 'drone_count': 0, + 'pass': 0, + 'fail': 0 + }) + + for report in report_files: + prefix = f'reports/{report["filename"]}/' + sub_blobs = self.bucket.list_blobs(prefix=prefix) + contains_fuzzy = False + drone_count = 0 + pass_count = 0 + fail_count = 0 + + for sub_blob in sub_blobs: + sub_parts = sub_blob.name.split('/') + + if len(sub_parts) == 4 and sub_blob.name.endswith('.txt'): + drone_count += 1 + + if len(sub_parts) < 3: + continue + monitor = sub_parts[2] + if 'fuzzy' in monitor.lower(): + contains_fuzzy = True + + if sub_blob.name.endswith('.txt'): + file_contents = sub_blob.download_as_text() + if "FAIL" in file_contents: + fail_count += 1 + elif "PASS" in file_contents: + pass_count += 1 + + report['contains_fuzzy'] = contains_fuzzy + report['drone_count'] = drone_count + report['pass'] = pass_count + report['fail'] = fail_count + + return {'reports': report_files} + + except Exception as e: + print(f"Error fetching reports from GCS: {e}") + return {'error': 'Failed to list reports from GCS'} + + def list_folder_contents(self, folder_name): + """Lists the contents of a specific report folder from GCS.""" + try: + prefix = f'reports/{folder_name}/' + blobs = self.bucket.list_blobs(prefix=prefix) + + result = { + "name": folder_name, + "UnorderedWaypointMonitor": [], + "CircularDeviationMonitor": [], + "CollisionMonitor": [], + "LandspaceMonitor": [], + "OrderedWaypointMonitor": [], + "PointDeviationMonitor": [], + "MinSepDistMonitor": [], + "NoFlyZoneMonitor": [], + "htmlFiles": [] + } + + fuzzy_folders = set() + for blob in blobs: + parts = blob.name.split('/') + if len(parts) < 3: + continue + monitor = parts[2] + if monitor.startswith("Fuzzy_Wind_"): + fuzzy_folders.add(monitor) + + if fuzzy_folders: + for fuzzy_folder in fuzzy_folders: + fuzzy_prefix = f'reports/{folder_name}/{fuzzy_folder}/' + self._process_gcs_directory(fuzzy_prefix, result, fuzzy_folder) + else: + self._process_gcs_directory(prefix, result, "") + + # Collect HTML files at any depth + html_blobs = self.bucket.list_blobs(prefix=prefix) + for html_blob in html_blobs: + if html_blob.name.endswith('.html'): + relative_path = os.path.relpath(html_blob.name, prefix) + proxy_url = f"/serve-html/{folder_name}/{relative_path.replace(os.sep, '/')}" + result["htmlFiles"].append({ + "name": os.path.basename(html_blob.name), + "path": relative_path.replace(os.sep, '/'), + "url": proxy_url + }) + + return result + + except Exception as e: + print(f"Error fetching folder contents from GCS: {e}") + return {'error': 'Failed to list folder contents from GCS'} + + def _process_gcs_directory(self, prefix, result, fuzzy_path_value): + """Processes blobs in a GCS directory and populates the result.""" + blobs = self.bucket.list_blobs(prefix=prefix) + for blob in blobs: + file_name = os.path.basename(blob.name) + if blob.name.endswith('.txt'): + file_contents = blob.download_as_text() + info_content = self._get_info_contents(file_contents, "INFO", {}) + pass_content = self._get_info_contents(file_contents, "PASS", {}) + fail_content = self._get_info_contents(file_contents, "FAIL", {}) + + file_data = { + "name": file_name, + "type": "text/plain", + "fuzzyPath": fuzzy_path_value, + "fuzzyValue": fuzzy_path_value.split("_")[-1] if fuzzy_path_value else "", + "content": file_contents, + "infoContent": info_content, + "passContent": pass_content, + "failContent": fail_content + } + + # Append to the appropriate monitor list + for monitor_key in result.keys(): + if monitor_key in blob.name and monitor_key != "htmlFiles": + result[monitor_key].append(file_data) + break + + elif blob.name.endswith('.png'): + image_content = blob.download_as_bytes() + encoded_string = base64.b64encode(image_content).decode('utf-8') + html_path = blob.name.replace("_plot.png", "_interactive.html") + + file_data = { + "name": file_name, + "type": "image/png", + "fuzzyPath": fuzzy_path_value, + "fuzzyValue": fuzzy_path_value.split("_")[-1] if fuzzy_path_value else "", + "imgContent": encoded_string, + "path": html_path + } + + # Append to the appropriate monitor list + for monitor_key in result.keys(): + if monitor_key in blob.name and monitor_key != "htmlFiles": + result[monitor_key].append(file_data) + break + + def _get_info_contents(self, file_contents, keyword, drone_map): + """Parses file contents to extract info based on the keyword.""" + content_array = file_contents.split("\n") + for content in content_array: + content_split = content.split(";") + if keyword in content and len(content_split) == 4: + key = content_split[2].strip() + value = content_split[3].strip() + if key not in drone_map: + drone_map[key] = [value] + else: + drone_map[key].append(value) + return drone_map + + def serve_html(self, folder_name, relative_path): + """Serves an HTML file from GCS.""" + try: + blob_path = f'reports/{folder_name}/{relative_path}' + blob = self.bucket.blob(blob_path) + + if not blob.exists() or not blob.name.endswith('.html'): + return None, 404 + + file_contents = blob.download_as_text() + return file_contents, 200 + + except Exception as e: + print(f"Error serving HTML file: {e}") + return None, 500 diff --git a/backend/PythonClient/multirotor/storage/gd_storage_service.py b/backend/PythonClient/multirotor/storage/gd_storage_service.py new file mode 100644 index 000000000..50235bc76 --- /dev/null +++ b/backend/PythonClient/multirotor/storage/gd_storage_service.py @@ -0,0 +1,461 @@ +from PythonClient.multirotor.storage.abstract.storage_service import StorageServiceInterface +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.http import MediaInMemoryUpload, MediaIoBaseDownload +import threading +import base64 +from io import BytesIO +import logging +import os + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +class GoogleDriveStorageService(StorageServiceInterface): + """Concrete class of StorageServiceInterface, used for uploading to Google Drive""" + + _lock = threading.Lock() # Class-level lock to prevent race conditions + + def __init__(self, folder_id='your_root_folder_id'): + """Initializes the Google Drive client.""" + SCOPES = ['https://www.googleapis.com/auth/drive'] + credentials_path = os.getenv('GDRIVE_CREDENTIALS_PATH', 'key.json') + self.credentials = service_account.Credentials.from_service_account_file( + credentials_path, scopes=SCOPES) + self.service = build('drive', 'v3', credentials=self.credentials) + self.folder_id = folder_id + + def upload_to_service(self, file_name, content, content_type='text/plain'): + """Uploads a file to Google Drive, creating folders as necessary.""" + try: + # Ensure content is bytes + if isinstance(content, str): + content = content.encode('utf-8') + + # Handle folder creation and navigation + parent_id = self.folder_id + path_parts = file_name.strip('/').split('/') + for folder_name in path_parts[:-1]: # All parts except the last (which is the file name) + with self._lock: # Acquire lock to prevent race conditions + folder_id = self._get_or_create_folder(folder_name, parent_id) + if not folder_id: + logger.error(f"Failed to get or create folder '{folder_name}' under parent ID '{parent_id}'") + return + parent_id = folder_id + + # Prepare the file metadata and media + file_metadata = { + 'name': path_parts[-1], + 'parents': [parent_id] + } + media = MediaInMemoryUpload(content, mimetype=content_type) + + # Upload the file + file = self.service.files().create( + body=file_metadata, + media_body=media, + fields='id' + ).execute() + logger.info(f"File '{file_name}' uploaded to Google Drive with ID: {file.get('id')}.") + except Exception as e: + logger.exception(f"Failed to upload file '{file_name}': {e}") + + def _get_or_create_folder(self, folder_name, parent_id): + """Retrieves the folder ID if it exists, or creates it if it doesn't.""" + try: + # Escaping single quotes in folder_name to prevent query issues + escaped_folder_name = folder_name.replace("'", "\\'") + + # Check if the folder already exists + query = ( + f"mimeType='application/vnd.google-apps.folder' and " + f"name='{escaped_folder_name}' and " + f"'{parent_id}' in parents and trashed=false" + ) + results = self.service.files().list( + q=query, + fields="files(id, name)", + spaces='drive' + ).execute() + items = results.get('files', []) + if items: + if len(items) > 1: + logger.warning(f"Multiple folders named '{folder_name}' found under parent ID '{parent_id}'. Using the first one.") + return items[0]['id'] + else: + # Folder does not exist; create it + file_metadata = { + 'name': folder_name, + 'mimeType': 'application/vnd.google-apps.folder', + 'parents': [parent_id] + } + folder = self.service.files().create( + body=file_metadata, + fields='id' + ).execute() + logger.info(f"Created folder '{folder_name}' with ID: {folder.get('id')}") + return folder.get('id') + except Exception as e: + logger.exception(f"Error while getting or creating folder '{folder_name}': {e}") + return None + + def list_reports(self): + """Lists all report batches from Google Drive.""" + try: + # Get all items under 'reports' folder (self.folder_id), including their paths + all_items = list(self._list_all_files_recursively(self.folder_id)) + + report_files = [] + + # Build the initial report_files list + for item in all_items: + parts = item['path'].split('/') + if len(parts) < 1: + continue + batch_folder = parts[0] + if batch_folder not in [report['filename'] for report in report_files]: + report_files.append({ + 'filename': batch_folder, + 'contains_fuzzy': False, + 'drone_count': 0, + 'pass': 0, + 'fail': 0 + }) + + for report in report_files: + prefix = report['filename'] + batch_items = [item for item in all_items if item['path'].startswith(prefix)] + contains_fuzzy = False + drone_count = 0 + pass_count = 0 + fail_count = 0 + + for item in batch_items: + parts = item['path'].split('/') + + if len(parts) == 3 and item['name'].endswith('.txt'): + drone_count += 1 + + if len(parts) < 2: + continue + monitor = parts[1] + if 'fuzzy' in monitor.lower(): + contains_fuzzy = True + + if item['mimeType'] != 'application/vnd.google-apps.folder' and item['name'].endswith('.txt'): + file_contents = self._download_file_content(item['id']) + if "FAIL" in file_contents: + fail_count += 1 + elif "PASS" in file_contents: + pass_count += 1 + + report['contains_fuzzy'] = contains_fuzzy + report['drone_count'] = drone_count + report['pass'] = pass_count + report['fail'] = fail_count + + logger.debug(f"Report '{report['filename']}': contains_fuzzy={contains_fuzzy}, drone_count={drone_count}, pass={pass_count}, fail={fail_count}") + + return {'reports': report_files} + + except Exception as e: + logger.exception(f"Error fetching reports from Google Drive: {e}") + return {'error': 'Failed to list reports from Google Drive'} + + def _list_all_files_recursively(self, parent_id, parent_path=''): + """Recursively lists all files and folders under a given parent folder ID, including their paths.""" + try: + page_token = None + while True: + response = self.service.files().list( + q=f"'{parent_id}' in parents and trashed=false", + fields="nextPageToken, files(id, name, mimeType, parents)", + spaces='drive', + pageToken=page_token + ).execute() + files = response.get('files', []) + for file in files: + file_name = file['name'] + file_id = file['id'] + mime_type = file['mimeType'] + file_path = f"{parent_path}/{file_name}" if parent_path else file_name + file_parent_id = parent_id + + # Yield the file or folder information + yield { + 'id': file_id, + 'name': file_name, + 'mimeType': mime_type, + 'path': file_path, + 'parent_id': file_parent_id + } + + if mime_type == 'application/vnd.google-apps.folder': + # Recurse into subfolder + yield from self._list_all_files_recursively(file_id, parent_path=file_path) + page_token = response.get('nextPageToken', None) + if page_token is None: + break + except Exception as e: + logger.exception(f"Error listing files under folder {parent_id}: {e}") + + def list_folder_contents(self, folder_name): + """Lists the contents of a specific report folder from Google Drive.""" + try: + # Find the folder ID of the folder_name under 'reports' (self.folder_id) + escaped_folder_name = folder_name.replace("'", "\\'") + query = ( + f"mimeType='application/vnd.google-apps.folder' and " + f"name='{escaped_folder_name}' and " + f"'{self.folder_id}' in parents and trashed=false" + ) + results = self.service.files().list( + q=query, + fields="files(id, name)", + spaces='drive' + ).execute() + items = results.get('files', []) + + if not items: + logger.error(f"Folder '{folder_name}' not found under reports.") + return {'error': f"Folder '{folder_name}' not found."} + + folder_id = items[0]['id'] + + # Initialize result structure + result = { + "name": folder_name, + "UnorderedWaypointMonitor": [], + "CircularDeviationMonitor": [], + "CollisionMonitor": [], + "LandspaceMonitor": [], + "OrderedWaypointMonitor": [], + "PointDeviationMonitor": [], + "MinSepDistMonitor": [], + "NoFlyZoneMonitor": [], + "htmlFiles": [] + } + + # List all files under this folder recursively, including their paths + all_files = list(self._list_all_files_recursively(folder_id)) + + # Identify fuzzy folders + fuzzy_folders = set() + for file in all_files: + parts = file['path'].split('/') + if len(parts) < 3: + continue + monitor = parts[1] + if monitor.startswith("Fuzzy_Wind_"): + fuzzy_folders.add((monitor, file['id'])) + + if fuzzy_folders: + for fuzzy_folder_name, fuzzy_folder_id in fuzzy_folders: + fuzzy_path_value = fuzzy_folder_name + self._process_gd_directory(fuzzy_folder_id, result, fuzzy_path_value) + else: + self._process_gd_directory(folder_id, result, "") + + # Collect HTML files at any depth + for file in all_files: + if file['name'].endswith('.html'): + # Ensure that the path starts with the folder_name to correctly remove it + expected_prefix = f"{folder_name}/" + if file['path'].startswith(expected_prefix): + relative_path = file['path'][len(expected_prefix):] + else: + # Handle cases where the path might not start with folder_name + relative_path = file['path'] + logger.warning(f"File path '{file['path']}' does not start with '{folder_name}/'") + + proxy_url = f"/serve-html/{folder_name}/{relative_path}" + result["htmlFiles"].append({ + "name": file['name'], + "path": relative_path, + "url": proxy_url + }) + + logger.info(f"Completed listing folder contents for '{folder_name}'.") + return result + + except Exception as e: + logger.exception(f"Error fetching folder contents from Google Drive: {e}") + return {'error': 'Failed to list folder contents from Google Drive'} + + def _process_gd_directory(self, parent_id, result, fuzzy_path_value): + """Processes files in a Google Drive directory and populates the result.""" + try: + # List all files under the parent_id recursively + all_files = list(self._list_all_files_recursively(parent_id)) + + for file in all_files: + file_name = file['name'] + file_id = file['id'] + mime_type = file['mimeType'] + file_path = file['path'] + + if mime_type == 'application/vnd.google-apps.folder': + continue # Folders are already processed recursively + + if file_name.endswith('.txt'): + file_contents = self._download_file_content(file_id) + info_content = self._get_info_contents(file_contents, "INFO", {}) + pass_content = self._get_info_contents(file_contents, "PASS", {}) + fail_content = self._get_info_contents(file_contents, "FAIL", {}) + + file_data = { + "name": file_name, + "type": "text/plain", + "fuzzyPath": fuzzy_path_value, + "fuzzyValue": fuzzy_path_value.split("_")[-1] if fuzzy_path_value else "", + "content": file_contents, + "infoContent": info_content, + "passContent": pass_content, + "failContent": fail_content + } + + # Append to the appropriate monitor list + appended = False + for monitor_key in result.keys(): + if monitor_key in file_path and monitor_key != "htmlFiles": + result[monitor_key].append(file_data) + appended = True + break + if not appended: + logger.warning(f"TXT file '{file_name}' did not match any monitor keys.") + + elif file_name.endswith('.png'): + image_content = self._download_file_content_as_bytes(file_id) + encoded_string = base64.b64encode(image_content).decode('utf-8') + html_path = file_name.replace("_plot.png", "_interactive.html") + + file_data = { + "name": file_name, + "type": "image/png", + "fuzzyPath": fuzzy_path_value, + "fuzzyValue": fuzzy_path_value.split("_")[-1] if fuzzy_path_value else "", + "imgContent": encoded_string, + "path": html_path + } + + # Append to the appropriate monitor list + appended = False + for monitor_key in result.keys(): + if monitor_key in file_path and monitor_key != "htmlFiles": + result[monitor_key].append(file_data) + appended = True + break + if not appended: + logger.warning(f"PNG file '{file_name}' did not match any monitor keys.") + + except Exception as e: + logger.exception(f"Error processing directory with ID '{parent_id}': {e}") + + def _get_info_contents(self, file_contents, keyword, drone_map): + """Parses file contents to extract info based on the keyword.""" + try: + content_array = file_contents.split("\n") + for content in content_array: + content_split = content.split(";") + if keyword in content and len(content_split) == 4: + key = content_split[2].strip() + value = content_split[3].strip() + if key not in drone_map: + drone_map[key] = [value] + else: + drone_map[key].append(value) + return drone_map + except Exception as e: + logger.exception(f"Error parsing info contents: {e}") + return drone_map + + def serve_html(self, folder_name, relative_path): + """Serves an HTML file from Google Drive.""" + try: + escaped_folder_name = folder_name.replace("'", "\\'") + query = ( + f"mimeType='application/vnd.google-apps.folder' and " + f"name='{escaped_folder_name}' and " + f"'{self.folder_id}' in parents and trashed=false" + ) + response = self.service.files().list( + q=query, + fields="files(id, name)", + spaces='drive' + ).execute() + items = response.get('files', []) + + if not items: + logger.error(f"Folder '{folder_name}' not found under 'reports'.") + return None, 404 + + parent_id = items[0]['id'] + + # Traverse the relative_path to find the file + path_parts = relative_path.strip('/').split('/') + file_id = None + for part in path_parts: + escaped_part = part.replace("'", "\\'") + query = ( + f"name='{escaped_part}' and " + f"'{parent_id}' in parents and trashed=false" + ) + response = self.service.files().list( + q=query, + fields="files(id, name, mimeType)", + spaces='drive' + ).execute() + items = response.get('files', []) + if not items: + logger.error(f"File or folder '{part}' not found under parent ID '{parent_id}'.") + return None, 404 + item = items[0] + parent_id = item['id'] + mime_type = item['mimeType'] + + # Check if the file is an HTML file + if mime_type == 'application/vnd.google-apps.folder' or not item['name'].endswith('.html'): + logger.error(f"File '{relative_path}' is not an HTML file.") + return None, 404 + + # Download the file contents + file_contents = self._download_file_content(item['id']) + return file_contents, 200 + + except Exception as e: + logger.exception(f"Error serving HTML file: {e}") + return None, 500 + + def _download_file_content(self, file_id): + """Downloads the content of a file from Google Drive.""" + try: + request = self.service.files().get_media(fileId=file_id) + fh = BytesIO() + downloader = MediaIoBaseDownload(fh, request) + done = False + while not done: + status, done = downloader.next_chunk() + fh.seek(0) + file_contents = fh.read().decode('utf-8') + logger.debug(f"Downloaded file content for file ID: {file_id}") + return file_contents + except Exception as e: + logger.exception(f"Error downloading file content: {e}") + return '' + + def _download_file_content_as_bytes(self, file_id): + """Downloads the content of a file as bytes from Google Drive.""" + try: + request = self.service.files().get_media(fileId=file_id) + fh = BytesIO() + downloader = MediaIoBaseDownload(fh, request) + done = False + while not done: + status, done = downloader.next_chunk() + fh.seek(0) + file_contents = fh.read() + logger.debug(f"Downloaded bytes for file ID: {file_id}") + return file_contents + except Exception as e: + logger.exception(f"Error downloading file content as bytes: {e}") + return b'' diff --git a/backend/PythonClient/multirotor/storage/storage_config.py b/backend/PythonClient/multirotor/storage/storage_config.py new file mode 100644 index 000000000..9744ca0ce --- /dev/null +++ b/backend/PythonClient/multirotor/storage/storage_config.py @@ -0,0 +1,24 @@ +import os + +def get_storage_service(): + """ + Returns an instance of the configured storage service. + Modify this function to switch between different storage services. + """ + # Only GCS is implemented for now + + storage_type = os.getenv('STORAGE_TYPE', 'gcs') # 'gcs' or 'gdrive' + + # For Google Cloud Storage + if storage_type == 'gcs': + from .gcs_storage_service import GCSStorageService + bucket_name = os.getenv('GCS_BUCKET_NAME', 'droneworld') + return GCSStorageService(bucket_name=bucket_name) + elif storage_type == 'gdrive': + from .gd_storage_service import GoogleDriveStorageService + folder_id = os.getenv('GDRIVE_FOLDER_ID', 'google drive folder ID') + return GoogleDriveStorageService(folder_id=folder_id) + + # For Google Drive Storage + # from .gd_storage_service import GoogleDriveStorageService + # return GoogleDriveStorageService(folder_id='1zZ06TaCzqdPJlQ9Uc4VgJaawBZm3eTSS') diff --git a/backend/PythonClient/multirotor/util/geo/geo_util.py b/backend/PythonClient/multirotor/util/geo/geo_util.py index 2003283ee..feb0d4615 100644 --- a/backend/PythonClient/multirotor/util/geo/geo_util.py +++ b/backend/PythonClient/multirotor/util/geo/geo_util.py @@ -1,5 +1,6 @@ import math import requests +import os class Vector: """ @@ -189,10 +190,21 @@ def get_distance_btw_3d_points(point_arr_1, point_arr_2): @staticmethod def get_elevation(lat, lng): - #curl -L -X GET 'https://maps.googleapis.com/maps/api/elevation/json?locations=39.7391536%2C-104.9847034&key=AIzaSyAZg02ECdzNzTvjTLbIRr61eh-P9mCq2ac' - url = f"https://maps.googleapis.com/maps/api/elevation/json?locations={lat}%2C{lng}&key=AIzaSyAZg02ECdzNzTvjTLbIRr61eh-P9mCq2ac" + #curl -L -X GET 'https://maps.googleapis.com/maps/api/elevation/json?locations={lat=39.7391536}%2C{long=-104.9847034}&key={api_key}' + api_key = os.getenv('GOOGLE_MAPS_API_KEY', 'google maps api key') + url = f"https://maps.googleapis.com/maps/api/elevation/json?locations={lat}%2C{lng}&key={api_key}" response = requests.get(url).json() - if 'results' in response: + + # Check if the request was successful + if response.get('status') != 'OK': + print(f"Error from API: {response.get('status')}") + print(f"API Response: {response}") + return None + + # Check if 'results' is in the response and not empty + if 'results' in response and response['results']: return response['results'][0]['elevation'] else: + print(f"No elevation data found for location: {lat}, {lng}") + print(f"API Response: {response}") return None \ No newline at end of file diff --git a/backend/PythonClient/multirotor/util/graph/three_dimensional_grapher.py b/backend/PythonClient/multirotor/util/graph/three_dimensional_grapher.py index 447ca9447..7a90cb6d0 100644 --- a/backend/PythonClient/multirotor/util/graph/three_dimensional_grapher.py +++ b/backend/PythonClient/multirotor/util/graph/three_dimensional_grapher.py @@ -2,24 +2,19 @@ from matplotlib import pyplot as plt import matplotlib import plotly.express as px -import os import threading +import io # For in-memory buffer -# create a lock object - +# Create a lock object lock = threading.Lock() -matplotlib.use('agg') - - -def setup_dir(dir): - if not os.path.exists(dir): - os.makedirs(dir) +matplotlib.use('agg') # Use non-interactive backend class ThreeDimensionalGrapher: + def __init__(self, storage_service): + self.storage_service = storage_service - @staticmethod - def draw_trace(actual_position_list, full_target_directory, drone_name, title): + def draw_trace(self, actual_position_list, drone_name, title, folder_path): with lock: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') @@ -27,60 +22,55 @@ def draw_trace(actual_position_list, full_target_directory, drone_name, title): y1 = [point[1] for point in actual_position_list] z1 = [-point[2] for point in actual_position_list] ax.plot(x1, y1, z1, label="Position trace") - # max_val = max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs))) - # ax.set_xlim([-max_val, max_val]) - # ax.set_ylim([-max_val, max_val]) - # ax.set_zlim([-max_val, max_val]) ax.legend() ax.set_box_aspect([1, 1, 1]) ax.set_xlabel('North (+X) axis') ax.set_ylabel('East (+Y) axis') ax.set_zlabel('Height (+Z) axis') - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_plot.png") - # print(file_name) plt.title(title) - plt.savefig(file_name, dpi=200, bbox_inches='tight') + # Save to in-memory buffer + buf = io.BytesIO() + plt.savefig(buf, format='png', dpi=200, bbox_inches='tight') + buf.seek(0) + content = buf.read() + buf.close() plt.close() fig.clf() + # Upload to storage service + file_name = f"{folder_path}{drone_name}_plot.png" + self.storage_service.upload_to_service(file_name, content, content_type='image/png') - @staticmethod - def draw_trace_vs_planned(planed_position_list, actual_position_list, full_target_directory, drone_name, - title): + def draw_trace_vs_planned(self, planed_position_list, actual_position_list, drone_name, title, folder_path): with lock: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') x1 = [point[0] for point in planed_position_list] y1 = [point[1] for point in planed_position_list] z1 = [-point[2] for point in planed_position_list] - ax.plot(x1, y1, z1, label="Planed") - + ax.plot(x1, y1, z1, label="Planned") x2 = [point[0] for point in actual_position_list] y2 = [point[1] for point in actual_position_list] z2 = [-point[2] for point in actual_position_list] ax.plot(x2, y2, z2, label="Actual") - # ax.set_box_aspect([1, 1, 1]) - # max_val = max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs))) - # max_val = max(max_val, max(abs(max(x2, key=abs)), abs(max(y2, key=abs)), abs(max(z2, key=abs)))) ax.set_box_aspect([1, 1, 1]) - # ax.set_xlim([-max_val, max_val]) - # ax.set_ylim([-max_val, max_val]) - # ax.set_zlim([-max_val, max_val]) ax.set_xlabel('North (+X) axis') ax.set_ylabel('East (+Y) axis') ax.set_zlabel('Height (+Z) axis') ax.legend() - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_plot.png") - # print(file_name) plt.title(title) - plt.savefig(file_name, dpi=200, bbox_inches='tight') + # Save to in-memory buffer + buf = io.BytesIO() + plt.savefig(buf, format='png', dpi=200, bbox_inches='tight') + buf.seek(0) + content = buf.read() + buf.close() plt.close() fig.clf() + # Upload to storage service + file_name = f"{folder_path}{drone_name}_plot.png" + self.storage_service.upload_to_service(file_name, content, content_type='image/png') - @staticmethod - def draw_trace_vs_point(destination_point, actual_position_list, full_target_directory, drone_name, - title): + def draw_trace_vs_point(self, destination_point, actual_position_list, drone_name, title, folder_path): with lock: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') @@ -88,112 +78,100 @@ def draw_trace_vs_point(destination_point, actual_position_list, full_target_dir y1 = destination_point[1] z1 = -destination_point[2] ax.plot(x1, y1, z1, marker="o", markersize=10, label="Destination") - x2 = [point[0] for point in actual_position_list] y2 = [point[1] for point in actual_position_list] z2 = [-point[2] for point in actual_position_list] - ax.plot(x2, y2, z2, label="Actual") - - # point_max = max(destination_point, key=abs) - # max_val = max(max( - # abs(max(x2, key=abs)), abs(max(y2, key=abs)), abs(max(z2, key=abs)))) - # max_val = max(max_val, point_max) - # - # ax.set_xlim([-max_val, max_val]) - # ax.set_ylim([-max_val, max_val]) - # ax.set_zlim([-max_val, max_val]) ax.set_xlabel('North (+X) axis') ax.set_ylabel('East (+Y) axis') ax.set_zlabel('Height (+Z) axis') ax.set_box_aspect([1, 1, 1]) ax.legend() - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_plot.png") plt.title(title) - plt.savefig(file_name, dpi=200, bbox_inches='tight') + # Save to in-memory buffer + buf = io.BytesIO() + plt.savefig(buf, format='png', dpi=200, bbox_inches='tight') + buf.seek(0) + content = buf.read() + buf.close() plt.close() fig.clf() + # Upload to storage service + file_name = f"{folder_path}{drone_name}_plot.png" + self.storage_service.upload_to_service(file_name, content, content_type='image/png') - @staticmethod - def draw_interactive_trace(actual_position, full_target_directory, drone_name, - title): + def draw_interactive_trace(self, actual_position, drone_name, title, folder_path): with lock: - actual = actual_position - fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') - x1 = [point[0] for point in actual] - y1 = [point[1] for point in actual] - z1 = [-point[2] for point in actual] + x1 = [point[0] for point in actual_position] + y1 = [point[1] for point in actual_position] + z1 = [-point[2] for point in actual_position] fig = px.scatter_3d(title=title) fig.add_scatter3d(x=x1, y=y1, z=z1, name=drone_name + " path") - # max_val = max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs))) - # fig.update_layout(title_text=title, - # scene=dict(xaxis_range=[-max_val, max_val], - # yaxis_range=[-max_val, max_val], - # zaxis_range=[-max_val, max_val])) - setup_dir(full_target_directory) - ax.set_xlabel('North (+X) axis') - ax.set_ylabel('East (+Y) axis') - ax.set_zlabel('Height (+Z) axis') - file_name = os.path.join(full_target_directory, str(drone_name) + "_interactive.html") - fig.write_html(file_name) - plt.close() - - @staticmethod - def draw_interactive_trace_vs_point(destination, actual_position, full_target_directory, drone_name, - title): + fig.update_layout( + scene=dict( + xaxis_title='North (+X) axis', + yaxis_title='East (+Y) axis', + zaxis_title='Height (+Z) axis', + aspectratio=dict(x=1, y=1, z=1) + ) + ) + # Save to HTML in-memory buffer + html_str = fig.to_html() + content = html_str.encode('utf-8') + # Upload to storage service + file_name = f"{folder_path}{drone_name}_interactive.html" + self.storage_service.upload_to_service(file_name, content, content_type='text/html') + + def draw_interactive_trace_vs_point(self, destination, actual_position, drone_name, title, folder_path): with lock: - actual = actual_position - fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') - x1 = [point[0] for point in actual] - y1 = [point[1] for point in actual] - z1 = [-point[2] for point in actual] - df = pd.DataFrame({'x': x1, 'y': y1, 'z': z1}) - fig = px.scatter_3d(df, x='x', y='y', z='z') - ax.set_xlabel('North (+X) axis') - ax.set_ylabel('East (+Y) axis') - ax.set_zlabel('Height (+Z) axis') - fig.add_scatter3d(x=[destination[0]], y=[destination[1]], z=[-destination[2]], name="Destination point", - marker=dict(size=10, symbol='circle')) - # max_val = max(abs(max(destination, key=abs))) - # max_val = max(max_val, max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs)))) - # fig.update_layout(scene=dict(xaxis_range=[-max_val, max_val], - # yaxis_range=[-max_val, max_val], - # zaxis_range=[-max_val, max_val])) - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_interactive.html") - fig.write_html(file_name) - plt.close() - - @staticmethod - def draw_interactive_trace_vs_planned(planed_position_list, actual_position_list, full_target_directory, - drone_name, - title): + x1 = [point[0] for point in actual_position] + y1 = [point[1] for point in actual_position] + z1 = [-point[2] for point in actual_position] + fig = px.scatter_3d(x=x1, y=y1, z=z1, title=title) + fig.add_scatter3d( + x=[destination[0]], + y=[destination[1]], + z=[-destination[2]], + name="Destination point", + marker=dict(size=10, symbol='circle') + ) + fig.update_layout( + scene=dict( + xaxis_title='North (+X) axis', + yaxis_title='East (+Y) axis', + zaxis_title='Height (+Z) axis', + aspectratio=dict(x=1, y=1, z=1) + ) + ) + # Save to HTML in-memory buffer + html_str = fig.to_html() + content = html_str.encode('utf-8') + # Upload to storage service + file_name = f"{folder_path}{drone_name}_interactive.html" + self.storage_service.upload_to_service(file_name, content, content_type='text/html') + + def draw_interactive_trace_vs_planned(self, planed_position_list, actual_position_list, drone_name, title, folder_path): with lock: - fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') x1 = [point[0] for point in actual_position_list] y1 = [point[1] for point in actual_position_list] z1 = [-point[2] for point in actual_position_list] x2 = [point[0] for point in planed_position_list] y2 = [point[1] for point in planed_position_list] z2 = [-point[2] for point in planed_position_list] - ax.set_xlabel('North (+X) axis') - ax.set_ylabel('East (+Y) axis') - ax.set_zlabel('Height (+Z) axis') - fig = px.scatter_3d(title=title) fig.add_scatter3d(x=x1, y=y1, z=z1, name="Actual") fig.add_scatter3d(x=x2, y=y2, z=z2, name="Planned") - # max_val = max(abs(max(x1, key=abs)), abs(max(y1, key=abs)), abs(max(z1, key=abs))) - # max_val = max(max_val, max(abs(max(x2, key=abs)), abs(max(y2, key=abs)), abs(max(z2, key=abs)))) - # fig.update_layout(title_text=title, - # scene=dict(xaxis_range=[-max_val, max_val], - # yaxis_range=[-max_val, max_val], - # zaxis_range=[-max_val, max_val])) - setup_dir(full_target_directory) - file_name = os.path.join(full_target_directory, str(drone_name) + "_interactive.html") - fig.write_html(file_name) - plt.close() + fig.update_layout( + scene=dict( + xaxis_title='North (+X) axis', + yaxis_title='East (+Y) axis', + zaxis_title='Height (+Z) axis', + aspectratio=dict(x=1, y=1, z=1) + ) + ) + # Save to HTML in-memory buffer + html_str = fig.to_html() + content = html_str.encode('utf-8') + # Upload to storage service + file_name = f"{folder_path}{drone_name}_interactive.html" + self.storage_service.upload_to_service(file_name, content, content_type='text/html') diff --git a/backend/PythonClient/server/cdf_server_delay_test.py b/backend/PythonClient/server/cdf_server_delay_test.py index 0219c844e..79dc8361a 100644 --- a/backend/PythonClient/server/cdf_server_delay_test.py +++ b/backend/PythonClient/server/cdf_server_delay_test.py @@ -1,8 +1,15 @@ import requests import time +import os + +WIND_SERVICE_HOST = os.getenv('WIND_SERVICE_HOST', '172.18.126.222') +WIND_SERVICE_PORT = os.getenv('WIND_SERVICE_PORT', '5001') + +# This is a mess waiting to be cleaned up. +post_url = f'http://{WIND_SERVICE_HOST}:{WIND_SERVICE_PORT}/wind' +get_url = f'http://{WIND_SERVICE_HOST}:{WIND_SERVICE_PORT}/' + -post_url = 'http://172.18.126.222:5001/wind' -get_url = 'http://172.18.126.222:5001/' def measure_delay_post(): start_time = time.time() diff --git a/backend/PythonClient/server/simulation_server.py b/backend/PythonClient/server/simulation_server.py index 87003b6b6..4fcf90a88 100644 --- a/backend/PythonClient/server/simulation_server.py +++ b/backend/PythonClient/server/simulation_server.py @@ -1,283 +1,208 @@ import logging -import os.path +import os import threading import time -import base64 import sys -from flask import Flask, request, abort, send_file, render_template, Response, jsonify +from flask import Flask, request, abort, render_template, Response, jsonify from flask_cors import CORS -##UNCOMMENT LINE IF TESTING ON LOCAL MACHINE +# Add parent directories to the Python path for module imports sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + +# Import the SimulationTaskManager from PythonClient.multirotor.control.simulation_task_manager import SimulationTaskManager +# Import the storage service from the configuration module +from PythonClient.multirotor.storage.storage_config import get_storage_service + app = Flask(__name__, template_folder="./templates") +# Configure logging to suppress Werkzeug logs except for errors log = logging.getLogger('werkzeug') log.setLevel(logging.ERROR) CORS(app) +# Initialize the SimulationTaskManager task_dispatcher = SimulationTaskManager() -threading.Thread(target=task_dispatcher.start).start() -task_number = 1 - - -# For Frontend to fetch all missions available to use -#@app.route('/mission', methods=['GET']) -#def mission(): -# directory = '../multirotor/mission' -# return [file for file in os.listdir(directory) if os.path.isfile(os.path.join(directory, file))] +threading.Thread(target=task_dispatcher.start, daemon=True).start() + +task_number = 1 # Global task counter + +# Initialize the storage service +storage_service = get_storage_service() +print(f"Using {storage_service.__class__.__name__} as the storage service.") + +# === Simulation Configuration State === +simulation_state = { + "environment": {}, + "monitors": {}, + "drones": [] +} + +# === New API Routes === + +@app.route('/api/simulation', methods=['GET']) +def get_simulation_state(): + """Retrieve the current simulation state.""" + return jsonify(simulation_state), 200 + +@app.route('/api/simulation/drones', methods=['POST']) +def add_drone(): + """Add a new drone to the simulation.""" + new_drone = request.get_json() + if not new_drone or "id" not in new_drone: + return jsonify({"error": "Invalid drone data"}), 400 + + simulation_state["drones"].append(new_drone) + return jsonify(new_drone), 201 + +@app.route('/api/simulation/drones/', methods=['PUT']) +def update_drone(drone_id): + """Update an existing drone's configuration.""" + updated_drone = request.get_json() + for i, drone in enumerate(simulation_state["drones"]): + if str(drone["id"]) == drone_id: + simulation_state["drones"][i] = updated_drone + return jsonify(updated_drone), 200 + return jsonify({"error": "Drone not found"}), 404 + +@app.route('/api/simulation/drones/', methods=['DELETE']) +def delete_drone(drone_id): + """Remove a drone from the simulation.""" + for i, drone in enumerate(simulation_state["drones"]): + if str(drone["id"]) == drone_id: + del simulation_state["drones"][i] + return jsonify({"message": "Drone deleted"}), 200 + return jsonify({"error": "Drone not found"}), 404 + +@app.route('/api/simulation/environment', methods=['PUT']) +def update_environment(): + """Update the simulation environment settings.""" + new_environment = request.get_json() + simulation_state["environment"] = new_environment + return jsonify({"message": "Environment updated"}), 200 + +@app.route('/api/simulation/monitors', methods=['PUT']) +def update_monitors(): + """Update the simulation monitor settings.""" + new_monitors = request.get_json() + simulation_state["monitors"] = new_monitors + return jsonify({"message": "Monitors updated"}), 200 + +# === Flask Routes === @app.route('/list-reports', methods=['GET']) def list_reports(): - # Reports file - reports_path = os.path.join(os.path.expanduser("~"), "Documents", "AirSim", "report") - - if not os.path.exists(reports_path) or not os.path.isdir(reports_path): - return 'Reports directory not found', 404 - - def count_pass_fail_from_log(directory): - pass_count = fail_count = 0 - # Navigate to the specific directory structure for GlobalMonitors -> MinSepDistMonitor -> log.txt - for root, dirs, files in os.walk(directory): - if 'GlobalMonitors' in dirs: - global_monitors_path = os.path.join(root, 'GlobalMonitors') - min_sep_dist_monitor_path = os.path.join(global_monitors_path, 'MinSepDistMonitor') - log_file_path = os.path.join(min_sep_dist_monitor_path, 'log.txt') - if os.path.exists(log_file_path): - with open(log_file_path, 'r') as log_file: - for line in log_file: - if 'PASS' in line: - try: - items_list = eval(line.split(';')[2]) - pass_count += len(items_list) - except SyntaxError: - pass # Handle potential eval errors safely - elif 'FAIL' in line: - try: - items_list = eval(line.split(';')[2]) - fail_count += len(items_list) - except SyntaxError: - pass - break # Stop searching once log.txt is found and processed - return pass_count, fail_count - - report_files = [] - for file in os.listdir(reports_path): - file_path = os.path.join(reports_path, file) - if os.path.isdir(file_path): - # Find 'Fuzzy' files - fuzzy_files = [f for f in os.listdir(file_path) if 'fuzzy' in f.lower()] - contains_fuzzy = len(fuzzy_files) > 0 - - # Determine the path to count Drone files - if contains_fuzzy: - first_fuzzy_path = os.path.join(file_path, fuzzy_files[0]) - if os.path.isdir(first_fuzzy_path): - flytopoints_path = os.path.join(first_fuzzy_path, 'FlyToPoints') - else: - flytopoints_path = os.path.join(file_path, 'FlyToPoints') - else: - flytopoints_path = os.path.join(file_path, 'FlyToPoints') - - # Count Drones - drone_count = 0 - if os.path.exists(flytopoints_path) and os.path.isdir(flytopoints_path): - drone_count = sum(1 for f in os.listdir(flytopoints_path) if f.startswith('FlyToPoints_Drone')) - - # Count PASS and FAIL from log.txt - pass_count, fail_count = count_pass_fail_from_log(file_path) - - report_files.append({ - 'filename': file, - 'contains_fuzzy': contains_fuzzy, - 'drone_count': drone_count, - 'pass': pass_count, - 'fail': fail_count - }) - else: - # For non-directory files, you could adjust handling if needed - report_files.append({ - 'filename': file, - 'contains_fuzzy': False, - 'drone_count': 0, - 'pass': 0, - 'fail': 0 - }) - - return {'reports': report_files} -""" -#old version without the pass fails -def list_reports(): - # Reports file - reports_path = os.path.join(os.path.expanduser("~"), "Documents", "AirSim", "report") - if not os.path.exists(reports_path) or not os.path.isdir(reports_path): - return 'Reports directory not found', 404 - #print("Listing items in:", reports_path) #Debugging line - #print(os.listdir(reports_path)) #Debugging line - report_files = [] - for file in os.listdir(reports_path): - if 'store' in file.lower(): - continue #skip ds store files entirely, we dont want them - - file_path = os.path.join(reports_path, file) - - if os.path.isdir(file_path): - #Find 'Fuzzy' files - fuzzy_files = [f for f in os.listdir(file_path) if 'fuzzy' in f.lower()] - contains_fuzzy = len(fuzzy_files) > 0 - #Determine the path to count Drone files - if contains_fuzzy: - first_fuzzy_path = os.path.join(file_path, fuzzy_files[0]) - #Check if the first 'Fuzzy' file is a directory - if os.path.isdir(first_fuzzy_path): - flytopoints_path = os.path.join(first_fuzzy_path, 'FlyToPoints') - else: - flytopoints_path = os.path.join(file_path, 'FlyToPoints') - else: - flytopoints_path = os.path.join(file_path, 'FlyToPoints') - #Count Drones - drone_count = 0 - if os.path.exists(flytopoints_path) and os.path.isdir(flytopoints_path): - drone_count = sum(1 for f in os.listdir(flytopoints_path) if f.startswith('FlyToPoints_Drone')) - report_files.append({'filename': file, 'contains_fuzzy': contains_fuzzy, 'drone_count': drone_count}) - else: - report_files.append({'filename': file, 'contains_fuzzy': False, 'drone_count': 0}) - return {'reports': report_files} -""" - -""" -@app.route('/list-reports', methods=['GET']) -def list_reports(): - # Reports file - reports_path = os.path.join(os.path.expanduser("~"), "Documents", "AirSim", "report") - if not os.path.exists(reports_path) or not os.path.isdir(reports_path): - return 'Reports directory not found', 404 - #print("Listing items in:", reports_path) #Debugging line - #print(os.listdir(reports_path)) #Debugging line - report_files = [] - for file in os.listdir(reports_path): - file_path = os.path.join(reports_path, file) - #print("Checking file:", file_path) - if os.path.isfile(file_path): - #contains_fuzzy = 'Fuzzy' in file - report_files.append({'filename': file}) - else: - report_files.append({'filename': file}) - return {'reports': report_files} - -@app.route('/get-file-path/', methods=['GET']) -def get_file_path(filename): - #construct the full path to the file - file_path = os.path.join(os.path.expanduser("~"), "Documents", "AirSim", "report", filename) - - #return the file path - return file_path -""" -""" -#make a report data function that takes the fileName. -@app.route('/report-data/', methods=['GET']) - -def report_data(filename): - - #construct the full path to the file - file_path = os.path.join(os.path.expanduser("~"), "Documents", "AirSim", "report", filename) - - #check if the file exists - if not os.path.exists(file_path): - return jsonify({'error': 'File not found'}), 404 + """ + Lists all report batches from the storage service. + """ + try: + reports = storage_service.list_reports() + if 'error' in reports: + return jsonify({'error': 'Failed to list reports'}), 500 + return jsonify(reports) + except Exception as e: + print(f"Error fetching reports: {e}") + return jsonify({'error': 'Failed to list reports'}), 500 +@app.route('/list-folder-contents/', methods=['POST']) +def list_folder_contents(folder_name): + """ + Lists the contents of a specific report folder from the storage service. + """ try: - #open and read the file content - with open(file_path, 'r') as file: - content = file.read() - return jsonify({'content': content}) - - #if error give us an error message to tell the user + folder_contents = storage_service.list_folder_contents(folder_name) + if 'error' in folder_contents: + return jsonify({'error': 'Failed to list folder contents'}), 500 + return jsonify(folder_contents) except Exception as e: - return jsonify({'error': 'Error reading file', 'details': str(e)}), 500 -""" - -@app.route('/list-folder-contents-', methods=['GET']) -def list_folder_contents(foldername): - base_directory = os.path.join(os.path.expanduser("~"), "Documents", "AirSim", "report") - folder_name = request.args.get(foldername) - folder_path = os.path.join(base_directory, folder_name) - - if not os.path.exists(folder_path) or not os.path.isdir(folder_path): - return jsonify({'error': 'Folder not found'}), 404 - - folder_contents = [] - for item in os.listdir(folder_path): - item_path = os.path.join(folder_path, item) - file_content = None - file_type = None - - if os.path.isfile(item_path): - file_type = 'file' - if item.endswith('.txt') or item.endswith('.html'): - # For text and HTML files, read as text - with open(item_path, 'r', encoding='utf-8') as file: - file_content = file.read() - elif item.endswith('.png'): - # For PNG images, encode the content in base64 - with open(item_path, 'rb') as file: - file_content = base64.b64encode(file.read()).decode('utf-8') - else: - # For other file types, you may add more conditions - continue - - folder_contents.append({ - 'name': item, - 'type': file_type, - 'content': file_content, - 'file_extension': item.split('.')[-1] - }) - elif os.path.isdir(item_path): - file_type = 'directory' - folder_contents.append({ - 'name': item, - 'type': file_type - }) - - return jsonify(folder_contents) + print(f"Error fetching folder contents: {e}") + return jsonify({'error': 'Failed to list folder contents'}), 500 +@app.route('/serve-html//', methods=['GET']) +def serve_html(folder_name, relative_path): + """ + Serves HTML files using the storage service. + """ + try: + file_contents, status_code = storage_service.serve_html(folder_name, relative_path) + if status_code == 200: + return Response(file_contents, mimetype='text/html') + elif status_code == 404: + return jsonify({"error": "HTML file not found"}), 404 + else: + return jsonify({"error": "Failed to serve HTML file"}), 500 + except Exception as e: + print(f"Error serving HTML file: {e}") + return jsonify({"error": "Failed to serve HTML file"}), 500 @app.route('/addTask', methods=['POST']) def add_task(): + """ + Adds a new simulation task to the queue. + """ global task_number - uuid_string = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) + "_Batch_" + str(task_number) - task_dispatcher.add_task(request.get_json(), uuid_string) - task_number += 1 - print(f"New task added to queue, currently {task_dispatcher.mission_queue.qsize()} in queue") - return uuid_string - + try: + task_data = request.get_json() + if not task_data: + return jsonify({'error': 'No task data provided'}), 400 + + # Generate a unique UUID string for the task + uuid_string = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) + "_Batch_" + str(task_number) + task_dispatcher.add_task(task_data, uuid_string) + task_number += 1 + print(f"New task added to queue, currently {task_dispatcher.mission_queue.qsize()} in queue") + return jsonify({'task_id': uuid_string}), 200 + except Exception as e: + print(f"Error adding task: {e}") + return jsonify({'error': 'Failed to add task'}), 500 @app.route('/currentRunning', methods=['GET']) def get_current_running(): + """ + Retrieves the current running task and the queue size. + """ current_task_batch = task_dispatcher.get_current_task_batch() if current_task_batch == "None": - return f"{'None'}, {task_dispatcher.mission_queue.qsize()}" + return jsonify({'current_task': 'None', 'queue_size': task_dispatcher.mission_queue.qsize()}), 200 else: - return f"{'Running'}, {task_dispatcher.mission_queue.qsize()}" + return jsonify({'current_task': 'Running', 'queue_size': task_dispatcher.mission_queue.qsize()}), 200 @app.route('/report') @app.route('/report/') def get_report(dir_name=''): - report_root_dir = os.path.join(os.path.expanduser("~"), "Documents", "AirSim", "report") - dir_path = os.path.join(report_root_dir, dir_name) - if not os.path.exists(dir_path): - return abort(404) - if os.path.isfile(dir_path): - return send_file(dir_path) - files = os.listdir(dir_path) - return render_template('files.html', files=files) + """ + Serves reports from the storage service. + """ + try: + if dir_name: + prefix = f'reports/{dir_name}/' + else: + prefix = 'reports/' + + # Assuming the storage service has a method to list files in a prefix + if hasattr(storage_service, 'list_files'): + files = storage_service.list_files(prefix) + else: + # If not implemented, return a 501 Not Implemented + return abort(501) + + if not files: + return abort(404) + + return render_template('files.html', files=files) + except Exception as e: + print(f"Error fetching report for directory {dir_name}: {e}") + return abort(404) @app.route('/stream//') def stream(drone_name, camera_name): - if task_dispatcher.unreal_state['state'] == 'idle': - return "No task running" + """ + Streams camera data for a specific drone and camera. + """ + if task_dispatcher.unreal_state.get('state') == 'idle': + return "No task running", 200 else: try: return Response( @@ -286,45 +211,27 @@ def stream(drone_name, camera_name): ) except Exception as e: print(e) - return "Error" - - -# @app.route('/uploadMission', methods=['POST']) -# def upload_file(): -# file = request.files['file'] -# filename = file.filename -# custom_mission_dir = '../multirotor/mission/custom' -# path = os.path.join(custom_mission_dir, filename) -# file.save(path) -# return 'File uploaded' - - -# def update_settings_json(drone_number, separation_distance): -# SettingGenerator(drone_number, separation_distance) - + return "Error", 500 @app.route('/state', methods=['GET']) def get_state(): """ - For unreal engine to check the current run state - :return: json obj consists of the current state with this specific format - { - "state": "idle" - } - or - { - "state": "start" - } - any other state will be not accepted by the unreal engine side and the change will be ignored + Returns the current state of the simulation. """ - return task_dispatcher.unreal_state - + return jsonify(task_dispatcher.unreal_state), 200 @app.route('/cesiumCoordinate', methods=['GET']) def get_map(): - return task_dispatcher.load_cesium_setting() + """ + Loads Cesium map settings. + """ + return task_dispatcher.load_cesium_setting(), 200 +@app.route('/api/health', methods=['GET']) +def health_check(): + return jsonify({"status": "ok", "message": "Backend is reachable!"}) +# === Run the Flask App === if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) - # makes it discoverable by other devices in the network + print("Starting DroneWorld API Server...") + app.run(host='0.0.0.0', port=5000) # Makes it discoverable by other devices in the networkecho diff --git a/backend/requirements.txt b/backend/requirements.txt index 4361e2ae6..647fdf065 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,7 +9,10 @@ flask_cors==4.0.0 scipy~=1.10.1 plotly_express msgpack-python~=0.5.6 -requests==2.31.0 +requests==2.32.4 pandas==2.0.2 plotly==5.15.0 -Ofpp~=0.11 \ No newline at end of file +Ofpp~=0.11 +google-cloud-storage==2.18.2 +google-auth==2.35.0 +google-api-python-client==2.149.0 diff --git a/credentials/fake-gcs-key.json b/credentials/fake-gcs-key.json new file mode 100644 index 000000000..1e55df3cd --- /dev/null +++ b/credentials/fake-gcs-key.json @@ -0,0 +1,11 @@ +{ + "type": "service_account", + "project_id": "test-project", + "private_key_id": "fake-key-id", + "private_key": "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----\n", + "client_email": "fake@test-project.iam.gserviceaccount.com", + "client_id": "123456789", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs" +} \ No newline at end of file diff --git a/dev.ps1 b/dev.ps1 new file mode 100644 index 000000000..a8f6be4b2 --- /dev/null +++ b/dev.ps1 @@ -0,0 +1,205 @@ + + Write-Host "If you get an execution policy error, run this once:" + Write-Host "" + Write-Host "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" + Write-Host "" + +# DroneWorld Development Helper Script (PowerShell) +# Usage: .\dev.ps1 [command] + +param( + [Parameter(Position=0)] + [string]$Command +) + +function Check-Token { + # Check if token is in environment + if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { + return $true + } + + # Check if token is in .env file and auto-export it + if (Test-Path ".env") { + $envContent = Get-Content ".env" + $tokenLine = $envContent | Where-Object { $_ -match "^GITHUB_TOKEN=" } + if ($tokenLine) { + $token = $tokenLine -replace "^GITHUB_TOKEN=", "" + if (-not [string]::IsNullOrWhiteSpace($token)) { + $env:GITHUB_TOKEN = $token + Write-Host "✅ Loaded GITHUB_TOKEN from .env" -ForegroundColor Green + return $true + } + } + } + + Write-Host "⚠️ GITHUB_TOKEN not found." -ForegroundColor Yellow + Write-Host "Run '.\dev.ps1 token' to set it up." -ForegroundColor Yellow + return $false +} + +function Set-Token { + Write-Host "🔑 Setting up GITHUB_TOKEN..." -ForegroundColor Green + Write-Host "" + + # Check if token already exists in .env + if (Test-Path ".env") { + $envContent = Get-Content ".env" + $tokenLine = $envContent | Where-Object { $_ -match "^GITHUB_TOKEN=" } + if ($tokenLine) { + $currentToken = $tokenLine -replace "^GITHUB_TOKEN=", "" + if (-not [string]::IsNullOrWhiteSpace($currentToken)) { + $preview = $currentToken.Substring(0, [Math]::Min(10, $currentToken.Length)) + Write-Host "✅ Found existing token in .env: $preview..." -ForegroundColor Green + $response = Read-Host "Use existing token? (Y/n)" + if ([string]::IsNullOrWhiteSpace($response) -or $response -match "^[Yy]$") { + $env:GITHUB_TOKEN = $currentToken + Write-Host "✅ Token exported for current session" -ForegroundColor Green + return + } + } + } + } + + # Prompt for new token + $secureToken = Read-Host "Enter your GitHub Personal Access Token" -AsSecureString + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken) + $token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) + + if ([string]::IsNullOrWhiteSpace($token)) { + Write-Host "❌ No token provided" -ForegroundColor Red + return + } + + # Set environment variable for current session + $env:GITHUB_TOKEN = $token + + # Save to .env file in root + $envFile = ".env" + $tokenLine = "GITHUB_TOKEN=$token" + + if (Test-Path $envFile) { + $content = Get-Content $envFile + $found = $false + $newContent = $content | ForEach-Object { + if ($_ -match "^GITHUB_TOKEN=") { + $found = $true + $tokenLine + } else { + $_ + } + } + + if ($found) { + $newContent | Set-Content $envFile + Write-Host "✅ Updated GITHUB_TOKEN in .env" -ForegroundColor Green + } else { + Add-Content $envFile "`n$tokenLine" + Write-Host "✅ Added GITHUB_TOKEN to .env" -ForegroundColor Green + } + } else { + $tokenLine | Set-Content $envFile + Write-Host "✅ Created .env with GITHUB_TOKEN" -ForegroundColor Green + } + + Write-Host "✅ Token exported for current session" -ForegroundColor Green +} + +function Print-Usage { + Write-Host "" + Write-Host "Usage: .\dev.ps1 [command]" -ForegroundColor Cyan + Write-Host "" + Write-Host "Commands:" -ForegroundColor Yellow + Write-Host " token - Set GITHUB_TOKEN for building simulator (required for 'full' and 'simulator')" + Write-Host " full - Start all services (frontend, backend, simulator)" + Write-Host " dev - Start development services only (frontend, backend)" + Write-Host " frontend - Start frontend only" + Write-Host " backend - Start backend only" + Write-Host " simulator - Start simulator only" + Write-Host " logs - Follow logs for dev services" + Write-Host " logs-all - Follow logs for all services" + Write-Host " stop - Stop all services" + Write-Host " stop-dev - Stop development services only" + Write-Host " clean - Stop and remove all containers and volumes" + Write-Host " help - Show this help message" + Write-Host "" + Write-Host "Examples:" -ForegroundColor Cyan + Write-Host " .\dev.ps1 token # Set GitHub token (needed before 'full' or 'simulator')" + Write-Host " .\dev.ps1 dev # Quick start for development" + Write-Host " .\dev.ps1 full # Start everything including simulator" + Write-Host "" + Write-Host "If you get an execution policy error, run this once:" -ForegroundColor Red + Write-Host "" + Write-Host "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" + Write-Host "" +} + +switch ($Command) { + "token" { + Set-Token + } + "full" { + if (-not (Check-Token)) { + Write-Host "" + $response = Read-Host "Continue without token? The simulator will fail to build. (y/N)" + if ($response -notmatch "^[Yy]$") { + exit 1 + } + } + Write-Host "🚀 Starting full stack (frontend + backend + simulator)..." -ForegroundColor Green + docker-compose up + } + "dev" { + Write-Host "🔧 Starting development services (frontend + backend only)..." -ForegroundColor Green + docker-compose -f docker-compose.dev.yaml up + } + "frontend" { + Write-Host "⚛️ Starting frontend only..." -ForegroundColor Green + docker-compose up frontend + } + "backend" { + Write-Host "🐍 Starting backend only..." -ForegroundColor Green + docker-compose up backend + } + "simulator" { + if (-not (Check-Token)) { + Write-Host "" + $response = Read-Host "Continue without token? The simulator will fail to build. (y/N)" + if ($response -notmatch "^[Yy]$") { + exit 1 + } + } + Write-Host "🎮 Starting simulator only..." -ForegroundColor Green + docker-compose up drv-unreal + } + "logs" { + Write-Host "📋 Following development service logs..." -ForegroundColor Green + docker-compose -f docker-compose.dev.yaml logs -f frontend backend + } + "logs-all" { + Write-Host "📋 Following all service logs..." -ForegroundColor Green + docker-compose logs -f + } + "stop" { + Write-Host "🛑 Stopping all services..." -ForegroundColor Yellow + docker-compose down + } + "stop-dev" { + Write-Host "🛑 Stopping development services..." -ForegroundColor Yellow + docker-compose -f docker-compose.dev.yaml down + } + "clean" { + Write-Host "🧹 Cleaning up all containers and volumes..." -ForegroundColor Yellow + docker-compose down -v + docker-compose -f docker-compose.dev.yaml down -v + Write-Host "✅ Cleanup complete" -ForegroundColor Green + } + { $_ -eq "help" -or $_ -eq "" -or $null -eq $_ } { + Print-Usage + } + default { + Write-Host "❌ Unknown command: $Command" -ForegroundColor Red + Print-Usage + exit 1 + } +} \ No newline at end of file diff --git a/dev.sh b/dev.sh new file mode 100755 index 000000000..55e95fcc6 --- /dev/null +++ b/dev.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# DroneWorld Development Helper Script + +set -e + +check_token() { + # Check if token is in environment + if [ -n "$GITHUB_TOKEN" ]; then + return 0 + fi + + # Check if token is in .env file and auto-export it + if [ -f .env ] && grep -q "^GITHUB_TOKEN=" .env; then + token=$(grep "^GITHUB_TOKEN=" .env | cut -d '=' -f2-) + if [ -n "$token" ]; then + export GITHUB_TOKEN="$token" + echo "✅ Loaded GITHUB_TOKEN from .env" + return 0 + fi + fi + + echo "⚠️ GITHUB_TOKEN not found." + echo "Run './dev.sh token' to set it up." + return 1 +} + +set_token() { + echo "🔑 Setting up GITHUB_TOKEN..." + echo "" + + # Check if token already exists in .env + if [ -f .env ] && grep -q "^GITHUB_TOKEN=" .env; then + current_token=$(grep "^GITHUB_TOKEN=" .env | cut -d '=' -f2-) + if [ -n "$current_token" ]; then + echo "✅ Found existing token in .env: ${current_token:0:10}..." + read -p "Use existing token? (Y/n): " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then + export GITHUB_TOKEN="$current_token" + echo "✅ Token exported for current session" + return 0 + fi + fi + fi + + # Prompt for new token + read -sp "Enter your GitHub Personal Access Token: " token + echo "" + + if [ -z "$token" ]; then + echo "❌ No token provided" + return 1 + fi + + export GITHUB_TOKEN="$token" + + # Save to .env file in root + if [ -f .env ] && grep -q "^GITHUB_TOKEN=" .env; then + # Update existing token + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed -i '' "s|^GITHUB_TOKEN=.*|GITHUB_TOKEN=$token|" .env + else + # Linux + sed -i "s|^GITHUB_TOKEN=.*|GITHUB_TOKEN=$token|" .env + fi + echo "✅ Updated GITHUB_TOKEN in .env" + else + # Add new token + echo "GITHUB_TOKEN=$token" >> .env + echo "✅ Added GITHUB_TOKEN to .env" + fi + + echo "✅ Token exported for current session" +} + +print_usage() { + echo "Usage: ./dev.sh [command]" + echo "" + echo "Commands:" + echo " token - Set GITHUB_TOKEN for building simulator (required for 'full' and 'simulator')" + echo " full - Start all services (frontend, backend, simulator)" + echo " dev - Start development services only (frontend, backend)" + echo " frontend - Start frontend only" + echo " backend - Start backend only" + echo " simulator - Start simulator only" + echo " logs - Follow logs for dev services" + echo " logs-all - Follow logs for all services" + echo " stop - Stop all services" + echo " stop-dev - Stop development services only" + echo " clean - Stop and remove all containers and volumes" + echo " help - Show this help message" + echo "" + echo "Examples:" + echo " ./dev.sh token # Set GitHub token (needed before 'full' or 'simulator')" + echo " ./dev.sh dev # Quick start for development" + echo " ./dev.sh full # Start everything including simulator" +} + +case "$1" in + token) + set_token + ;; + full) + if ! check_token; then + echo "" + read -p "Continue without token? The simulator will fail to build. (y/N): " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + fi + echo "🚀 Starting full stack (frontend + backend + simulator)..." + docker-compose up + ;; + dev) + echo "🔧 Starting development services (frontend + backend only)..." + docker-compose -f docker-compose.dev.yaml up + ;; + frontend) + echo "⚛️ Starting frontend only..." + docker-compose up frontend + ;; + backend) + echo "🐍 Starting backend only..." + docker-compose up backend + ;; + simulator) + if ! check_token; then + echo "" + read -p "Continue without token? The simulator will fail to build. (y/N): " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + fi + echo "🎮 Starting simulator only..." + docker-compose up drv-unreal + ;; + logs) + echo "📋 Following development service logs..." + docker-compose -f docker-compose.dev.yaml logs -f frontend backend + ;; + logs-all) + echo "📋 Following all service logs..." + docker-compose logs -f + ;; + stop) + echo "🛑 Stopping all services..." + docker-compose down + ;; + stop-dev) + echo "🛑 Stopping development services..." + docker-compose -f docker-compose.dev.yaml down + ;; + clean) + echo "🧹 Cleaning up all containers and volumes..." + docker-compose down -v + docker-compose -f docker-compose.dev.yaml down -v + echo "✅ Cleanup complete" + ;; + help|"") + print_usage + ;; + *) + echo "❌ Unknown command: $1" + echo "" + print_usage + exit 1 + ;; +esac \ No newline at end of file diff --git a/docker-compose-init.sh b/docker-compose-init.sh new file mode 100644 index 000000000..5b0699404 --- /dev/null +++ b/docker-compose-init.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Wait for fake-gcs to be ready +until curl -s http://fake-gcs:4443/storage/v1/b > /dev/null; do + echo "Waiting for fake-gcs-server..." + sleep 2 +done + +# Create bucket +curl -X POST http://fake-gcs:4443/storage/v1/b \ + -H "Content-Type: application/json" \ + -d '{"name":"droneworld"}' + +echo "Bucket 'droneworld' created" \ No newline at end of file diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 000000000..06ec5c1d2 --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,79 @@ +# Development docker-compose that excludes the simulator +# Use this when working on frontend/backend without needing the Unreal simulation + +name: DroneWorld +services: + backend: + image: droneworld/droneworld-backend:v1.0.0 + build: ./backend + ports: + - "5000:5000" + volumes: + - ./backend:/app + - ./credentials:/app/credentials + - ./config/airsim:/root/Documents/AirSim + depends_on: + fake-gcs: + condition: service_healthy + env_file: + - ./backend/.env + environment: + - DRV_UNREAL_HOST=drv-unreal + - DRV_UNREAL_API_PORT=3001 + - DRV_PIXELSTREAM_PORT=8888 + networks: + - droneworld-network + + frontend: + image: droneworld/droneworld-frontend:v1.0.0 + build: ./frontend + ports: + - "3000:3000" + volumes: + - ./frontend:/app + - /app/node_modules + environment: + - CHOKIDAR_USEPOLLING=true + - PIXELSTREAM_URL=ws://localhost:8888 + - REACT_APP_BACKEND_URL=http://localhost:5000 + depends_on: + - backend + networks: + - droneworld-network + + fake-gcs: + image: fsouza/fake-gcs-server + ports: + - "4443:4443" + command: -scheme http -port 4443 -external-url http://fake-gcs:4443 -log-level error + volumes: + - ./fake-gcs-data:/data + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:4443/storage/v1/b"] + interval: 5s + timeout: 3s + retries: 5 + networks: + - droneworld-network + + init-storage: + image: curlimages/curl:latest + depends_on: + fake-gcs: + condition: service_healthy + command: + - sh + - -c + - | + curl -X POST http://fake-gcs:4443/storage/v1/b \ + -H 'Content-Type: application/json' \ + -d '{"name":"droneworld"}' + networks: + - droneworld-network + +networks: + droneworld-network: + driver: bridge + +volumes: + fake-gcs-data: \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 000000000..83495f682 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,130 @@ +name: DroneWorld +services: + backend: + image: droneworld/droneworld-backend:v1.0.0 + build: ./backend + ports: + - "5000:5000" + volumes: + - ./backend:/app # Mount backend code + - ./credentials:/app/credentials # Mount credentials folder + - ./config/airsim:/root/Documents/AirSim # Mount the AirSim folder + depends_on: + fake-gcs: + condition: service_healthy + env_file: + - ./backend/.env # Load environment variables from .env file + environment: + # DRV-Unreal service connection + - FLASK_ENV=development # Enable debug mode + - FLASK_DEBUG=1 # Enable hot reload + - DRV_UNREAL_HOST=drv-unreal + - DRV_UNREAL_API_PORT=3001 # DRV API port (if available) + - DRV_PIXELSTREAM_PORT=8888 + networks: + - droneworld-network + + frontend: + image: droneworld/droneworld-frontend:v1.0.0 + build: ./frontend + ports: + - "3000:3000" # Frontend keeps port 3000 + volumes: + - ./frontend:/app # Mount frontend code for live editing + - /app/node_modules # Separate node_modules to avoid host overwrite + environment: + - CHOKIDAR_USEPOLLING=true # Necessary for live-reload in Docker + - CHOKIDAR_INTERVAL=1000 # Poll every 1 second + - WATCHPACK_POLLING=true # Webpack 5 polling + - FAST_REFRESH=true # Ensure fast refresh is on + # PixelStreaming connection - frontend connects directly to DRV + - PIXELSTREAM_URL=ws://localhost:8888 + - BACKEND_URL=http://backend:5000 + - REACT_APP_BACKEND_URL=http://localhost:5000 # For frontend to reach backend + depends_on: + - backend + networks: + - droneworld-network + + drv-unreal: + image: droneworld/drv-unreal:v2.0.0 + build: + context: ./sim # Build context is now sim directory + dockerfile: Dockerfile + args: + VERSION: v2.0.0 + GITHUB_REPO: UAVLab-SLU/DRV-Unreal + GITHUB_TOKEN: ${GITHUB_TOKEN} # Pass token from environment + env_file: + - ./.env # Load environment variables from sim/.env for runtime + ports: + - "3001:3000" # Map DRV's port 3000 to host 3001 to avoid conflict + - "8888:8888" # PixelStreaming - exposed for direct frontend access + command: + - "./Blocks.sh" + - "-server" + - "-log" + - "-nullrhi" + - "-nosound" + - "-unattended" + - "-Port=3000" + environment: + - DISPLAY=${DISPLAY:-:0} + volumes: + - /tmp/.X11-unix:/tmp/.X11-unix:ro # X11 display support + - drv-data:/app/data # Persistent data for simulation results + - ./config/airsim:/home/ue4/Documents/AirSim:ro + # Uncomment below for GPU access (requires nvidia-docker2) + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] + networks: + - droneworld-network + healthcheck: + test: ["CMD", "sh", "-c", "netstat -an | grep -q 3000 || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + fake-gcs: + image: fsouza/fake-gcs-server + ports: + - "4443:4443" + command: -scheme http -port 4443 -external-url http://fake-gcs:4443 -log-level error + volumes: + - ./fake-gcs-data:/data + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:4443/storage/v1/b"] + interval: 5s + timeout: 3s + retries: 5 + networks: + - droneworld-network + + init-storage: + image: curlimages/curl:latest + depends_on: + fake-gcs: + condition: service_healthy + command: + - sh + - -c + - | + curl -X POST http://fake-gcs:4443/storage/v1/b \ + -H 'Content-Type: application/json' \ + -d '{"name":"droneworld"}' + networks: + - droneworld-network + +networks: + droneworld-network: + driver: bridge + +volumes: + fake-gcs-data: + drv-data: \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 000000000..4895671a7 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,13 @@ +# Ignore local logs +npm-debug.log +yarn-debug.log + +# Ignore temporary files +*.log +.cache +*.tmp +.DS_Store +Thumbs.db + +# Ignore Git repository +.git diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 000000000..6f6c6cbd1 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,2 @@ +REACT_APP_DEMO_USER_EMAIL='demo@sade.com' +REACT_APP_CESIUM_ION_ACCESS_TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI5N2U4YzMxOC1mN2Q1LTQ1MjItODc4Mi0yZmFmNzI2ODUxNDQiLCJpZCI6MjU5MjU5LCJpYXQiOjE3NDExMjYzNTl9.njn5GBu30Cx4PA4nXnrKLwX-phgN_Y9sYRNG2JgOq6g' \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index f91193441..2cbea7cb2 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,6 +1,7 @@ { "env": { "browser": true, + "node": true, "es2021": true }, "extends": [ diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index d3ff5fc90..000000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -package-lock.json \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..1518ec1e1 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,20 @@ +# Use an official Node.js runtime as a parent image +FROM node:20 + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install the necessary dependencies +RUN npm install + +# Copy the rest of the application code +COPY . . + +# Expose the port the app runs on +EXPOSE 3000 + +# Command to start the frontend +CMD ["npm", "start"] \ No newline at end of file diff --git a/frontend/DroneWorld b/frontend/DroneWorld new file mode 160000 index 000000000..808fe55ac --- /dev/null +++ b/frontend/DroneWorld @@ -0,0 +1 @@ +Subproject commit 808fe55ac28ff6257d32a7418736808b350a7508 diff --git a/frontend/babel.config.js b/frontend/babel.config.js new file mode 100644 index 000000000..7bbb2267f --- /dev/null +++ b/frontend/babel.config.js @@ -0,0 +1,10 @@ +module.exports = { + presets: [ + "@babel/preset-env", + "@babel/preset-react" + ], + plugins: [ + "@babel/plugin-transform-runtime" + ] + }; + \ No newline at end of file diff --git a/frontend/craco.config.js b/frontend/craco.config.js new file mode 100644 index 000000000..7d196e088 --- /dev/null +++ b/frontend/craco.config.js @@ -0,0 +1,18 @@ +module.exports = { + plugins: [ + { + plugin: require("craco-cesium")() + } + ], + webpack: { + configure: (webpackConfig) => { + // Enable polling for file changes in Docker + webpackConfig.watchOptions = { + poll: 1000, // Check for changes every second + aggregateTimeout: 300, // Delay before rebuilding + ignored: /node_modules/, + }; + return webpackConfig; + }, + }, +}; \ No newline at end of file diff --git a/frontend/cypress.config.js b/frontend/cypress.config.js new file mode 100644 index 000000000..c6e6630f7 --- /dev/null +++ b/frontend/cypress.config.js @@ -0,0 +1,14 @@ +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + e2e: { + baseUrl: 'http://localhost:3000', + setupNodeEvents(on, config) { + // implement node event listeners here + }, + specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', + + defaultCommandTimeout: 10000, + }, +}); + diff --git a/frontend/cypress/e2e/app.cy.js b/frontend/cypress/e2e/app.cy.js new file mode 100644 index 000000000..58404d513 --- /dev/null +++ b/frontend/cypress/e2e/app.cy.js @@ -0,0 +1,54 @@ +// Cypress Test for E2E Frontend Flow +describe('DroneWorld Application Flow', () => { + it('should complete the full scenario configuration flow', () => { + // Mock the backend API call so tests can run without the backend + cy.intercept('GET', 'http://localhost:5000/list-reports', { + statusCode: 200, + body: { reports: [] } + }).as('listReports'); + + // Step 1: Visit the landing page + cy.visit('/'); + + // Wait for the API call to complete (mocked response) + cy.wait('@listReports'); + + // Step 2: Wait for the "Get Started" button to be visible and click it + // Material-UI Button with component={Link} renders as an tag, not + > - {/* Main content area */} -
-

Welcome to Drone World!

-
- + darker blue + color: '#fff', + py: { xs: 8, md: 12 }, + }} + > + + + Advanced Drone +
+ Simulation Platform +
+ + + Create realistic 3D environments, test multi-drone scenarios, and analyze + performance with our comprehensive drone simulation platform. + + + - -
-
- - {/* About Us Modal */} - setOpen(false)} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" - > - - {/* Close Button */} - - - {/* Title Content */} - - -

- About Drone World -

-

- Drone World is revolutionizing sUAS (small Uncrewed Aerial Systems) testing. In the dynamic world of sUAS, safety and reliability are paramount. Traditional field testing across diverse environments is costly and challenging. -

-

- Drone World offers an innovative sUAV simulation ecosystem that generates high-fidelity, realistic environments mimicking real-world complexities like adverse weather and wireless interference. Our automated solution allows developers to specify constraints and generate tailored test environments. -

-

- The program monitors sUAV activities against predefined safety parameters and generates detailed acceptance test reports. This approach provides actionable insights for effective debugging and analysis, enhancing the safety, reliability, and efficiency of sUAS applications. -

-
+ + + + +
+ + + + + + + Powerful Simulation Features + + + + Everything you need to develop, test, and optimize drone operations in a + safe, virtual environment. + + + + {/* Card 1 */} + + + + + + + + + 3D Environment Generation + + + Create realistic terrains, cities, and landscapes for + comprehensive drone testing scenarios. + + + + + + + {/* Card 2 */} + + + + + + + + + Real-time Simulation + + + Monitor and control multiple drones simultaneously with live + data streaming and analytics. + + + + + + + {/* Card 3 */} + + + + + + + + + Multi-drone Coordination + + + Test swarm intelligence and formation flight patterns with + advanced coordination algorithms. + + + + + + + {/* Card 4 */} + + + + + + + + + Data Analytics + + + Comprehensive reporting and analysis tools to evaluate drone + performance and mission success. + + + + + + + + + + + + + + + Ready to start simulating? + + + Create your first test scenario today. + + + + + + + + + + + -
+ + + {isLoading ? ( + + ) : ( +
+ {filesPresent && ( +
+

+ +
{/* Content here */}
+ +

+ +
+ )} +
+ )} ); } diff --git a/frontend/src/components/BackendHealthTitle.jsx b/frontend/src/components/BackendHealthTitle.jsx new file mode 100644 index 000000000..8dee2e31a --- /dev/null +++ b/frontend/src/components/BackendHealthTitle.jsx @@ -0,0 +1,67 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +const BackendHealthTitle = ({ classes }) => { + const [isHealthy, setIsHealthy] = useState(true); + const [isChecking, setIsChecking] = useState(true); + + useEffect(() => { + const checkBackendHealth = async () => { + const backendUrl = process.env.REACT_APP_BACKEND_URL || 'http://localhost:5000'; + + try { + const response = await fetch(`${backendUrl}/api/health`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + setIsHealthy(true); + } else { + setIsHealthy(false); + } + } catch (error) { + console.error('Backend health check failed:', error); + setIsHealthy(false); + } finally { + setIsChecking(false); + } + }; + + // Check immediately on mount + checkBackendHealth(); + + // Optional: Check periodically (every 30 seconds) + const interval = setInterval(checkBackendHealth, 30000); + + // Cleanup interval on unmount + return () => clearInterval(interval); + }, []); + + const titleStyle = { + color: isHealthy ? 'inherit' : 'red', + transition: 'color 0.3s ease', + }; + + return ( + + Drone World 🚁 + + ); +}; + +BackendHealthTitle.propTypes = { + classes: PropTypes.shape({ + siteTitle: PropTypes.string.isRequired, + }).isRequired, +}; + +export default BackendHealthTitle; \ No newline at end of file diff --git a/frontend/src/components/Configuration/ControlsDisplay.jsx b/frontend/src/components/Configuration/ControlsDisplay.jsx new file mode 100644 index 000000000..18eeaef16 --- /dev/null +++ b/frontend/src/components/Configuration/ControlsDisplay.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Box, Grid, Typography } from '@mui/material'; +import PropTypes from 'prop-types'; + +const mapControlDisplay = ({ mapControl }) => { + if (!mapControl) { + return null; + } + return ( + + + + {mapControl.header} + + + + + {mapControl.body.map((control, index) => ( + + + {control.icon.map((iconUrl, idx) => ( + Icon + ))} + + + {control.command} + + {control.info} + + ))} + + + + ); +}; + +mapControlDisplay.propTypes = { + mapControl: PropTypes.object, +}; +export default mapControlDisplay; \ No newline at end of file diff --git a/frontend/src/components/Configuration/DroneConfiguration.jsx b/frontend/src/components/Configuration/DroneConfiguration.jsx index ff8c2d13b..84bb77b89 100644 --- a/frontend/src/components/Configuration/DroneConfiguration.jsx +++ b/frontend/src/components/Configuration/DroneConfiguration.jsx @@ -1,345 +1,308 @@ -import * as React from 'react' +import * as React from 'react'; import Grid from '@mui/material/Grid'; import TextField from '@mui/material/TextField'; import Container from '@mui/material/Container'; -import Box from '@mui/material/Box' -import Button from '@mui/material/Button' +import Box from '@mui/material/Box'; import InputLabel from '@mui/material/InputLabel'; import MenuItem from '@mui/material/MenuItem'; import FormControl from '@mui/material/FormControl'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; -import SensorConfiguration from './SensorConfiguration' +import Select from '@mui/material/Select'; +import SensorConfiguration from './SensorConfiguration'; import Tooltip from '@mui/material/Tooltip'; import Snackbar from '@mui/material/Snackbar'; import Alert from '@mui/material/Alert'; +import { useMainJson } from '../../contexts/MainJsonContext'; +import { SimulationConfigurationModel } from '../../model/SimulationConfigurationModel'; const flightPaths = [ - {value:'fly_in_circle', label:'Circle', id:1}, - {value:'fly_to_points', label:'Square', id:1}, - // {value:'fly_straight',label:'Straight', id:1} -] + {value: 'fly_in_circle', label: 'Circle', id: 1}, + {value: 'fly_to_points', label: 'Square', id: 1}, +]; const droneTypes = [ - {value:'FixedWing', label:'Fixed Wing'}, - {value:'MultiRotor', label:'Multi Rotor'} -] + {value: 'FixedWing', label: 'Fixed Wing'}, + {value: 'MultiRotor', label: 'Multi Rotor'} +]; -{/* Old stuff from draft PR - -const droneModels = [ +const droneModels = { + FixedWing: [ + {value: 'SenseflyeBeeX', label: 'Sensefly eBee X'}, + {value: 'TrinityF90', label: 'Trinity F90'} + ], + MultiRotor: [ {value: 'ParrotANAFI', label: 'Parrot ANAFI'}, {value: 'DJI', label: 'DJI'}, {value: 'StreamLineDesignX189', label: 'StreamLineDesign X189'} -] - -*/} - -//{/* -const droneModels = { - FixedWing: [ - {value: 'SenseflyeBeeX', label: 'Sensefly eBee X'}, - {value: 'TrinityF90', label: 'Trinity F90'} - ], - MultiRotor: [ - {value: 'ParrotANAFI', label: 'Parrot ANAFI'}, - {value: 'DJI', label: 'DJI'}, - {value: 'StreamLineDesignX189', label: 'StreamLineDesign X189'} - ] -} - -//*/} -const locations = [ - {value:'GeoLocation', id:1}, - {value:'Cartesian Coordinate', id:2} -] - -export default function DroneConfiguration (droneData) { - console.log('DroneConfiguration-----', droneData) - const [selectedLoc, setSelectedLoc] = React.useState('GeoLocation') - const [selectedModel, setSelectedModel] = React.useState(''); - const [selectedDroneType, setselectedDroneType] = React.useState(droneTypes[1].value); - const [drone, setDrone] = React.useState({ - ...droneData.droneObject, - // droneType: droneTypes[1].value - } - // != null ? droneData.droneObject : { - // VehicleType: "SimpleFlight", - // DefaultVehicleState: "Armed", - // EnableCollisionPassthrogh: false, - // EnableCollisions: true, - // AllowAPIAlways: true, - // EnableTrace: false, - // Name:droneData.name, - // droneName: droneData.name, - // X:0, - // Y:0, - // Z:0, - // Pitch: 0, - // Roll: 0, - // Yaw: 0, - // Sensors: null, - // MissionValue:null - // Mission : { - // name:"fly_to_points", - // param : [] - // }, - // // Cameras: { - // // CaptureSettings: [ - // // { - // // ImageType: 0, - // // Width: 256, - // // Height: 144, - // // FOV_Degrees: 90, - // // AutoExposureSpeed: 100, - // // AutoExposureBias: 0, - // // AutoExposureMaxBrightness: 0.64, - // // AutoExposureMinBrightness: 0.03, - // // MotionBlurAmount: 0, - // // TargetGamma: 1, - // // ProjectionMode: '', - // // OrthoWidth: 5.12 - // // } - // // ], - // // NoiseSettings: [ - // // { - // // Enabled: false, - // // ImageType: 0, - // // RandContrib: 0.2, - // // RandSpeed: 100000, - // // RandSize: 500, - // // RandDensity: 2, - // // HorzWaveContrib: 0.03, - // // HorzWaveStrength: 0.08, - // // HorzWaveVertSize: 1, - // // HorzWaveScreenSize: 1, - // // HorzNoiseLinesContrib: 1, - // // HorzNoiseLinesDensityY: 0.01, - // // HorzNoiseLinesDensityXY: 0.5, - // // HorzDistortionContrib: 1, - // // HorzDistortionStrength: 0.002 - // // } - // // ], - // // Gimbal: { - // // Stabilization: 0, - // // Pitch: 0, - // // Roll: 0, - // // Yaw: 0 - // // }, - // // X:0, - // // Y:0, - // // Z:0, - // // Pitch: 0, - // // Roll: 0, - // // Yaw: 0 - // // }} - ) - - const handleLocChange = (event ) => { - setSelectedLoc(event.target.value); - }; - - const handleMissionChange = (event ) => { - setDrone(prevState => ({ - ...prevState, - Mission: { - ...prevState.Mission, - name: event.target.value - } - })); - }; + ] +}; - const handleDroneTypeChange = (event) => { - handleSnackBarVisibility(true) - setselectedDroneType(event.target.value) - setDrone(prevState => ({ - ...prevState, - droneType: event.target.value - })); +export default function DroneConfiguration(droneData) { + const { mainJson, setMainJson } = useMainJson(); + const { name = "", id = "", droneObject = {}, resetName = () => {}, droneJson = () => {} } = droneData || {}; + + const [selectedLoc, setSelectedLoc] = React.useState('GeoLocation'); + const [selectedModel, setSelectedModel] = React.useState(''); + const [selectedDroneType, setselectedDroneType] = React.useState(droneTypes[1].value); + const [snackBarState, setSnackBarState] = React.useState({ open: false }); + + const [drone, setDrone] = React.useState(() => { + const defaults = { + VehicleType: "SimpleFlight", + DefaultVehicleState: "Armed", + EnableCollisionPassthrogh: false, + EnableCollisions: true, + AllowAPIAlways: true, + EnableTrace: false, + Name: name, + droneName: name, + X: 0, + Y: 0, + Z: 0, + Pitch: 0, + Roll: 0, + Yaw: 0, + Sensors: null, + MissionValue: null, + Mission: { + name: "fly_to_points", + param: [] + } }; + return { ...defaults, ...(droneData?.getDroneBasedOnIndex?.(id) || {}), ...droneObject }; + }); - const handleDroneModelChange = (event) => { - handleSnackBarVisibility(true) - setSelectedModel(event.target.value); - setDrone(prevState => ({ - ...prevState, - droneModel: event.target.value - })); + const syncDroneLocation = React.useCallback((x, y, z) => { + const updatedDrone = { + ...(droneData?.getDroneBasedOnIndex?.(id) || drone), + X: x, + Y: y, + Z: z }; + + if (droneData?.updateDroneBasedOnIndex) { + droneData.updateDroneBasedOnIndex(id, updatedDrone); + } + + setDrone(updatedDrone); + droneJson(updatedDrone, id); + }, [droneData, id, droneJson, drone]); + + const dropHandler = React.useCallback((e) => { + e.preventDefault(); + // Safely calculate position + const canvas = e.currentTarget; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + syncDroneLocation(x, y, 0); + }, [syncDroneLocation]); + - - - - const handleChange = (val) => { - console.log('handlechange---', val) - if(val.target.id == "Name"){ - droneData.resetName(val.target.value, droneData.id) - setDrone(prevState => ({ - ...prevState, - droneName: val.target.value - })) - } - setDrone(prevState => ({ - ...prevState, - [val.target.id]: val.target.type === "number" ? parseFloat(val.target.value) : val.target.value - })) + // Set up event listeners + React.useEffect(() => { + const canvas = document.getElementById('drone-config-canvas'); + if (canvas) { + canvas.addEventListener('drop', dropHandler); + canvas.addEventListener('dragover', (e) => e.preventDefault()); + + return () => { + canvas.removeEventListener('drop', dropHandler); + canvas.removeEventListener('dragover', (e) => e.preventDefault()); + }; } + }, [dropHandler]); + + // Sync position changes from external updates + React.useEffect(() => { + if (!droneData?.droneObject) return; + + setDrone(prev => ({ + ...prev, + X: droneData.droneObject.X ?? prev.X, + Y: droneData.droneObject.Y ?? prev.Y, + Z: droneData.droneObject.Z ?? prev.Z + })); + }, [droneData?.droneObject?.X, droneData?.droneObject?.Y, droneData?.droneObject?.Z]); - React.useEffect(() => { - sendJson() - }, [drone]) - const sendJson = () => { - droneData.droneJson(drone, droneData.id); - } + const handleMissionChange = (event) => { + setDrone(prevState => ({ + ...prevState, + Mission: { + ...prevState.Mission, + name: event.target.value + } + })); + }; - const setSensorConfig = (sensor) => { - setDrone(prevState => ({ - ...prevState, - Sensors: sensor - })) - console.log('sensor---in droneConfig', drone) - } + const handleDroneTypeChange = (event) => { + handleSnackBarVisibility(true); + setselectedDroneType(event.target.value); + setDrone(prevState => ({ + ...prevState, + droneType: event.target.value + })); + }; - const setCameraSettings = (camera) => { - console.log('camera---in drone', camera) - // setDrone(prevState => ({ - // ...prevState, - // Cameras: { - // ...prevState.Cameras, - // CaptureSettings: [camera] - // } - // })) - } + const handleDroneModelChange = (event) => { + handleSnackBarVisibility(true); + setSelectedModel(event.target.value); + setDrone(prevState => ({ + ...prevState, + droneModel: event.target.value + })); + }; - const [snackBarState, setSnackBarState] = React.useState({ - open: false, - }); - - const handleSnackBarVisibility = (val) => { - setSnackBarState(prevState => ({ - ...prevState, - open: val - })) + const handleChange = (val) => { + let drone = mainJson.getDroneBasedOnIndex(id) + if (val.target.id === "Name") { + drone.droneName = val.target.value; + resetName(val.target.value, id); + setDrone(prevState => ({ + ...prevState, + droneName: val.target.value + })); } - return ( -
- handleSnackBarVisibility(false)}> - handleSnackBarVisibility(false)} severity="info" sx={{ width: '100%' }}> - Drone Type and Drone Model Changes is under Developement ! - - - - - - - - + setDrone(prevState => ({ + ...prevState, + [val.target.id]: val.target.type === "number" ? parseFloat(val.target.value) : val.target.value + })); + drone[val.target.id] = val.target.type === "number" ? parseFloat(val.target.value) : val.target.value; + mainJson.updateDroneBasedOnIndex(id, drone); + setMainJson(SimulationConfigurationModel.getReactStateBasedUpdate(mainJson)); + }; - - - Mission - - - + const setSensorConfig = (sensor) => { + setDrone(prevState => ({ + ...prevState, + Sensors: sensor + })); + }; - {/*Drone Type selections */} + const handleSnackBarVisibility = (val) => { + setSnackBarState(prevState => ({ + ...prevState, + open: val + })); + }; - - - Drone Type - - - + return ( +
+ handleSnackBarVisibility(false)}> + handleSnackBarVisibility(false)} severity="info" sx={{ width: '100%' }}> + Drone Type and Drone Model Changes are under Development! + + + + + + + + + - {/*The Drone Models */} - - - Drone Model - - - + + + Mission + + + - {/* */} + + + Drone Type + + + + + + Drone Model + + + - {/*The Bottom Row of stuff */} - - {/* */} - - Home Location - {/* */} - + + {!(drone.X) && ( + Home Location + )} - {/* */} - {selectedLoc == 'GeoLocation' ? - - - - - - - - - - - - - - - - - : - - - - - - - - - - - - } - + {selectedLoc === 'GeoLocation' ? + <> + + + + + + + + - - - {/*     */} - {/* */} + + + + - - -
- ) + + : + <> + + + + + + + + + + + } +
+ + +
+
+
+ ); } \ No newline at end of file diff --git a/frontend/src/components/Configuration/MissionConfiguration.jsx b/frontend/src/components/Configuration/MissionConfiguration.jsx index 146fc39b9..6e0c2d4f6 100644 --- a/frontend/src/components/Configuration/MissionConfiguration.jsx +++ b/frontend/src/components/Configuration/MissionConfiguration.jsx @@ -13,7 +13,10 @@ import DroneConfiguration from './DroneConfiguration' import Alert from '@mui/material/Alert'; import AlertTitle from '@mui/material/AlertTitle'; import Grid from '@mui/material/Grid'; - +import Tooltip from '@mui/material/Tooltip'; +import { imageUrls } from '../../utils/const'; +import { useMainJson } from '../../contexts/MainJsonContext'; +import { SimulationConfigurationModel } from '../../model/SimulationConfigurationModel'; const useStyles = makeStyles((theme) => ({ root: { @@ -23,6 +26,8 @@ const useStyles = makeStyles((theme) => ({ })); export default function MissionConfiguration (mission) { + const { mainJson, setMainJson } = useMainJson(); + const [duplicateNameIndex, setDuplicateNameIndex] = React.useState(-1); const classes = useStyles(); const [droneCount, setDroneCount] = React.useState(mission.mainJsonValue.Drones != null ? mission.mainJsonValue.Drones.length : 1); const [droneArray, setDroneArray] = React.useState(mission.mainJsonValue.Drones != null ? mission.mainJsonValue.Drones : [{ @@ -100,6 +105,14 @@ export default function MissionConfiguration (mission) { // Yaw: 0 // } }]); + + React.useEffect(() => { + if(droneArray.length===1){ + mainJson.addNewDrone(droneArray[droneCount-1]); + setMainJson(SimulationConfigurationModel.getReactStateBasedUpdate(mainJson)); + } + }, []); + const setDrone = () => { droneArray.push({ id: (droneCount), @@ -176,8 +189,21 @@ export default function MissionConfiguration (mission) { // Yaw: 0 // } }) + mainJson.addNewDrone(droneArray[droneCount]); + setMainJson(SimulationConfigurationModel.getReactStateBasedUpdate(mainJson)); } + const handleDragStart = (event, index) => { + const imgSrc = event.target.src; + const dragData = { + type: 'drone', + src: imgSrc, + index: index, + }; + + event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); + }; + const popDrone = () =>{ droneArray.pop() } @@ -188,10 +214,35 @@ export default function MissionConfiguration (mission) { } const handleDecrement = () => { - setDroneCount(droneCount -1) - popDrone() - } - + // Ensure droneCount is greater than 0 to prevent negative counts + if (droneCount > 0) { + setDroneCount(prevCount => prevCount - 1); + + // Update the drone array by removing the last element + setDroneArray(prevArray => { + const updatedArray = prevArray.slice(0, -1); // Creates a new array without the last element + + // Call mainJson.popLastDrone() if it exists + if (mainJson && typeof mainJson.popLastDrone === 'function') { + mainJson.popLastDrone(); + } else { + console.warn('mainJson.popLastDrone is not a function'); + } + + // Safely update mainJson state + if (typeof SimulationConfigurationModel?.getReactStateBasedUpdate === 'function') { + setMainJson(SimulationConfigurationModel.getReactStateBasedUpdate(mainJson)); + } else { + console.warn('SimulationConfigurationModel.getReactStateBasedUpdate is not a function'); + } + + return updatedArray; + }); + } else { + console.warn('Drone count is already at zero.'); + } + }; + const setDroneName = (e, index) => { setDroneArray(objs => { return objs.map((obj, i) => { @@ -204,6 +255,13 @@ export default function MissionConfiguration (mission) { return obj }) }) + + const updatedDrone = mainJson.getDroneBasedOnIndex(index); + if (updatedDrone) { + updatedDrone.droneName = e; + mainJson.updateDroneBasedOnIndex(index, updatedDrone); + setMainJson(SimulationConfigurationModel.getReactStateBasedUpdate(mainJson)); + } } React.useEffect(() => { @@ -233,7 +291,7 @@ export default function MissionConfiguration (mission) { } return ( - + {/* */} Configure sUAS (small unmanned aircraft system) or drone characteristics in your scenario @@ -261,7 +319,32 @@ export default function MissionConfiguration (mission) { aria-controls="panel1a-content" id="panel1a-header" > - {drone.droneName} + + {drone.droneName} + + + + + + + Draggable Icon handleDragStart(e, index)} + style={{ width: 40, cursor: 'grab', marginRight: 20 }} + /> + + + + diff --git a/frontend/src/components/Configuration/SensorConfiguration.jsx b/frontend/src/components/Configuration/SensorConfiguration.jsx index 178f8b0db..5976fd2e2 100644 --- a/frontend/src/components/Configuration/SensorConfiguration.jsx +++ b/frontend/src/components/Configuration/SensorConfiguration.jsx @@ -1,9 +1,9 @@ -import * as React from 'react' +import * as React from 'react'; import Box from '@mui/material/Box'; import Modal from '@mui/material/Modal'; import Grid from '@mui/material/Grid'; -import Button from '@mui/material/Button' -import ButtonGroup from '@mui/material/ButtonGroup' +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; import Lidar from './SensorsConfiguration/Lidar'; import Barometer from './SensorsConfiguration/Barometer'; import IMU from './SensorsConfiguration/IMU'; @@ -17,28 +17,30 @@ import RadarOutlinedIcon from '@mui/icons-material/RadarOutlined'; import DeveloperBoardOutlinedIcon from '@mui/icons-material/DeveloperBoardOutlined'; import ExploreOutlinedIcon from '@mui/icons-material/ExploreOutlined'; import Fab from '@mui/material/Fab'; -import BottomNavigationAction from '@mui/material/BottomNavigationAction' +import BottomNavigationAction from '@mui/material/BottomNavigationAction'; import BottomNavigation from '@mui/material/BottomNavigation'; import RouteIcon from '@mui/icons-material/Route'; -import Distance from './SensorsConfiguration/Distance' - +import Distance from './SensorsConfiguration/Distance'; const style = { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 800, - bgcolor: 'background.paper', - border: '2px solid #000', - boxShadow: 24, - p: 4, + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 800, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, }; -export default function SensorConfiguration (param) { - const [visibleSensor, setVisibleSensor]= React.useState(null); - const [sensor, setSensor] = React.useState(param.sensorJson != null ? param.sensorJson : { - Barometer: { +export default function SensorConfiguration(param) { + const [visibleSensor, setVisibleSensor] = React.useState(null); + const [sensor, setSensor] = React.useState( + param.sensorJson != null + ? param.sensorJson + : { + Barometer: { SensorType: 1, Enabled: true, PressureFactorSigma: 0.001825, @@ -47,22 +49,32 @@ export default function SensorConfiguration (param) { UpdateLatency: 0, StartupDelay: 0, UpdateFrequency: 50, - Key:"Barometer" - }, - // IMU: null, - // Lidar: null, - Magnetometer:{ + Key: 'Barometer', + }, + IMU: { + SensorType: 2, + Enabled: true, + AngularRandomWalk: 0.3, + GyroBiasStabilityTau: 500, + GyroBiasStability: 4.6, + VelocityRandomWalk: 0.24, + AccelBiasStabilityTau: 800, + AccelBiasStability: 36, + Key: 'IMU', + }, + // Lidar: null, + Magnetometer: { SensorType: 4, Enabled: true, NoiseSigma: 0.005, ScaleFactor: 1, - NoiseBias: 0, - UpdateLatency: 0, - StartupDelay: 0, + NoiseBias: 0, + UpdateLatency: 0, + StartupDelay: 0, UpdateFrequency: 50, - Key:"Magnetometer" - }, - GPS:{ + Key: 'Magnetometer', + }, + GPS: { SensorType: 3, Enabled: true, EphTimeConstant: 0.9, @@ -76,118 +88,155 @@ export default function SensorConfiguration (param) { UpdateLatency: 0.2, StartupDelay: 1, UpdateFrequency: 50, - Key: "GPS" + Key: 'GPS', + }, + // Distance:null }, - // Distance:null - }) - const [open, setOpen] = React.useState(false); - const handleOpen = (sensr) => { - setOpen(true); - setVisibleSensor(sensr); + ); + const [open, setOpen] = React.useState(false); + const handleOpen = (sensr) => { + setOpen(true); + setVisibleSensor(sensr); + }; + const handleClose = (e, name) => { + if (name !== 'Camera') { + const updatedSensor = { + ...sensor, + [name]: e, + }; + setSensor(updatedSensor); + param.setSensor(updatedSensor); + } else { + param.setCamera(e); } - const handleClose = (e, name) => { - if(name != "Camera") { - setSensor(prevState => ({ - ...prevState, - [name]: e - })) - param.setSensor(sensor) - } else { - param.setCamera(e) - } - setOpen(false) - }; + setOpen(false); + }; - const updateSensorOjb = (e, name) => { - console.log('in sensor config--- updateSensorOjb----', e, 'gghgg---', name) - if(name != "Camera") { - setSensor(prevState => ({ - ...prevState, - [name]: e - })) - param.setSensor(sensor) - } else { - param.setCamera(e) - } + const updateSensorOjb = (e, name) => { + console.log('in sensor config--- updateSensorOjb----', e, 'gghgg---', name); + if (name !== 'Camera') { + const updatedSensor = { + ...sensor, + [name]: e, + }; + setSensor(updatedSensor); + param.setSensor(updatedSensor); + } else { + param.setCamera(e); } + }; - React.useEffect(() => { - console.log('use effect in sensor config') - param.setSensor(sensor) - }, [sensor]) + React.useEffect(() => { + console.log('use effect in sensor config'); + param.setSensor(sensor); + }, [sensor]); - const configBtns = [ - // { - // name:'Lidar', - // id:1, - // icon:, - // comp: - // }, - { - name:'Camera', - id:2, - icon:, - comp: - }, - { - name:'Barometer', - id:3, - icon:, - comp: - }, - { - name:'Magnetometer', - id:4, - icon:, - comp: - }, - // { - // name:'IMU', - // id:5, - // icon:, - // comp: - // }, - { - name:'GPS', - id:6, - icon:, - comp: - }, - // { - // name:'Distance', - // id:7, - // icon:, - // comp: - // } - ]; + const configBtns = [ + // { + // name:'Lidar', + // id:1, + // icon:, + // comp: + // }, + { + name: 'Camera', + id: 2, + icon: , + comp: , + }, + { + name: 'Barometer', + id: 3, + icon: , + comp: ( + + ), + }, + { + name: 'Magnetometer', + id: 4, + icon: , + comp: ( + + ), + }, + { + name: 'IMU', + id: 5, + icon: , + comp: ( + + ), + }, + { + name: 'GPS', + id: 6, + icon: , + comp: ( + + ), + }, + // { + // name:'Distance', + // id:7, + // icon:, + // comp: + // } + ]; - return ( -
- - {configBtns.map(function(btns) { - return (
- - - - - -
)}) - } -
- - - {visibleSensor != null ? visibleSensor.comp : ''} - - -
- ) -} \ No newline at end of file + return ( +
+ + {configBtns.map(function (btns) { + return ( +
+ + + + + +
+ ); + })} +
+ + {visibleSensor != null ? visibleSensor.comp : ''} + +
+ ); +} diff --git a/frontend/src/components/Configuration/SensorsConfiguration/IMU.jsx b/frontend/src/components/Configuration/SensorsConfiguration/IMU.jsx index a80ebff5f..09acdf290 100644 --- a/frontend/src/components/Configuration/SensorsConfiguration/IMU.jsx +++ b/frontend/src/components/Configuration/SensorsConfiguration/IMU.jsx @@ -8,79 +8,166 @@ import FormGroup from '@mui/material/FormGroup'; import FormControlLabel from '@mui/material/FormControlLabel'; import Switch from '@mui/material/Switch'; +export default function IMU(sensor) { + const [imu, setImu] = React.useState(sensor.imuObj || {}); + React.useEffect(() => { + sensor.updateJson(imu, sensor.name); + }, [imu]); -export default function IMU (sensor) { - const [imu, setImu] = React.useState({ - SensorType: 2, - Enabled: true, - // AngularRandomWalk: 0.3, - // GyroBiasStabilityTau: 500, - // GyroBiasStability: 4.6, - // VelocityRandomWalk: 0.24, - // AccelBiasStabilityTau: 800, - // AccelBiasStability: 36, - Key: 'IMU' - }) + const closeModal = () => { + // sends local state to the parent. + // fixed async state issue in SensorConfiguration.jsx's handleClose function. + sensor.closeModal(imu, sensor.name); + }; - const closeModal = () => { - sensor.closeModal(imu, sensor.name) - } + const handleChangeSwitch = (val) => { + setImu((prevState) => ({ + ...prevState, + Enabled: val.target.checked, + })); + }; - const handleChangeSwitch = (val) => { - setImu(prevState => ({ - ...prevState, - Enabled: val.target.checked - })) - } + const handleChange = (event) => { + const { id, value } = event.target; + const parsedValue = event.target.type === 'number' ? parseFloat(value) : value; - const handleChange = (val) => { - setImu(prevState => ({ - ...prevState, - [val.target.id]: val.target.value - })) - } + setImu((prevImu) => ({ + ...prevImu, + [id]: parsedValue, + })); + }; - return( -
- - - {imu.Key} - - - - - - } label="Enabled" /> - - - - - - {/* - - - - - - - - - - - - - - - - - */} - - -     - - - -
- ) -} \ No newline at end of file + const handleReset = () => { + setImu(sensor.imuObj); + }; + + return ( +
+ + {imu.Key || 'IMU'} + + + + + + + } + label='Enabled' + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/frontend/src/components/Configuration/SimulationConfigs.jsx b/frontend/src/components/Configuration/SimulationConfigs.jsx new file mode 100644 index 000000000..df878c678 --- /dev/null +++ b/frontend/src/components/Configuration/SimulationConfigs.jsx @@ -0,0 +1,293 @@ +import React, { Fragment, useEffect, useState } from 'react'; +import { Button, Typography, Grid, Box, Divider, CircularProgress } from '@mui/material'; +import { useLocation, useNavigate } from 'react-router-dom'; +import IconButton from '@mui/material/IconButton'; +import DeleteIcon from '@mui/icons-material/Delete'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CancelIcon from '@mui/icons-material/Cancel'; +import NoEntryIcon from '@mui/icons-material/Block'; +import Icon from '@mdi/react'; +import { mdiQuadcopter, mdiWeatherWindy } from '@mdi/js'; +import { callAPI } from '../../utils/ApiUtils'; +import { StyledBackground, ConfigsBox, ProfilesDivider } from '../../css/commonStyles'; +import { BootstrapTooltip } from '../../css/muiStyles'; +import { StyledLink, StyledButton } from '../../css/commonStyles'; +import { numberStyle, labelStyle, iconStyle } from '../../css/simConfigListStyles'; +import { isTokenExpired } from '../../utils/authUtils'; +import useSessionManager from '../../hooks/useSessionManager'; + +function SimulationConfigs() { + const navigate = useNavigate(); + const location = useLocation(); + const { handleSessionExpiration } = useSessionManager(); + const [configs, setConfigs] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedConfig, setSelectedConfig] = useState(null); + + const fetchConfigs = async (page = 1, resetList = false) => { + if (isTokenExpired()) { + handleSessionExpiration(location, null); + return; + } + + setLoading(true); + + if (resetList) { + setConfigs([]); + } + + try { + const endPoint = `api/sade_task/pagination/${page}`; + const data = await callAPI(endPoint, 'GET', null, 'JSON'); + if (data.length > 0) { + setConfigs((prevConfigs) => [...prevConfigs, ...data]); + fetchConfigs(page + 1); + } else { + setLoading(false); + } + } catch (error) { + console.error('Failed to fetch configs:', error); + setLoading(false); + } + }; + + useEffect(() => { + fetchConfigs(1, true); + }, []); + + const toggleDescription = (id) => { + setSelectedConfig(selectedConfig === id ? null : id); + }; + + const handleDelete = async (configId) => { + try { + if (isTokenExpired()) { + handleSessionExpiration(location, null); + return; + } + + const endPoint = `api/sade_task/${configId}`; + await callAPI(endPoint, 'DELETE', null, 'JSON'); + setConfigs(configs.filter((config) => config.id !== configId)); + } catch (error) { + console.error('Error deleting config:', error); + } + }; + + const handleStop = async (configId) => { + try { + if (isTokenExpired()) { + handleSessionExpiration(location, null); + return; + } + + const endPoint = `api/sade_task/terminate/${configId}`; + callAPI(endPoint, 'POST', null, 'JSON') + .then((data) => { + fetchConfigs(1, true); + }) + .catch((error) => { + console.error('Error Stopping config names:', error); + }); + // setConfigs(configs.filter((config) => config.id !== configId)); + } catch (error) { + console.error('Error Stopping config:', error); + } + }; + + const handleSelect = async (config) => { + try { + navigate('/simulation', { state: { configData: config } }); + } catch (error) { + console.error('Error Navigating to /simulation page:', error); + } + }; + + if (loading) { + return ( + + + Loading, please wait... + + ); + } + + return ( + + + {configs.length > 0 ? ( + + + SELECT PROFILE + + + + ) : ( + + + No Previous Configurations Available + + + + CREATE NEW SIM CONFIg + + + + )} + + + {configs.map((config) => ( + + + + + + + toggleDescription(config.id)} + > + + {config.name} + + + + + + + {config.date_created} + + + + + + {selectedConfig === config.id && ( + <> + + + + + + Wind Sources + + + + {config.wind_source_count} + + + + + + + Sade Zones + + + + {config.sade_zone_count} + + + + + + + Drones + + + + {config.drone_count} + + + + + + + Origin Lat: + + {config.origin_latitude} + + + + Origin Long: + + {config.origin_longitude} + + + + + + User Description: + + {config.description || 'NA'} + + + + )} + + + + + + + + handleSelect(config)} + sx={{ color: 'white' }} + > + + + + + + + + + handleDelete(config.id)} + sx={{ color: 'white' }} + > + + + + + + + + + handleStop(config.id)} + sx={{ color: 'white' }} + disabled={!config.task_running} + > + + + + + + + ))} + + + + ); +} + +export default SimulationConfigs; diff --git a/frontend/src/components/Configuration/SimulationController.jsx b/frontend/src/components/Configuration/SimulationController.jsx new file mode 100644 index 000000000..ed0977670 --- /dev/null +++ b/frontend/src/components/Configuration/SimulationController.jsx @@ -0,0 +1,275 @@ +import { useState, useRef, Fragment, useEffect } from 'react'; +import { CircularProgress, Box, Grid } from '@mui/material'; +import MissionConfiguration from './MissionConfiguration'; +import EnvironmentConfiguration from './EnvironmentConfiguration'; +import CesiumMap from '../cesium/CesiumMap'; +import MonitorControl from '../MonitorControl'; +import { useNavigate, useLocation } from 'react-router-dom'; +import HomeIcon from '@mui/icons-material/Home'; +import { useMainJson } from '../../contexts/MainJsonContext'; +import { StyledTab, StyledTabs } from '../../css/SimulationPageStyles'; +import { callAPI } from '../../utils/ApiUtils'; +import { mapControls } from '../../constants/map'; +import ControlsDisplay from './ControlsDisplay'; +import { BootstrapTooltip, StyledButton } from '../../css/muiStyles'; +import SaveConfigModal from '../SaveConfigModal'; +import { steps } from '../../constants/simConfig'; +import { mapEnvironmentData } from '../../utils/mapper/envMapper'; +import { mapDroneData } from '../../utils/mapper/droneMapper'; +import { isTokenExpired } from '../../utils/authUtils'; +import useSessionManager from '../../hooks/useSessionManager'; + +export default function SimulationController() { + // START of DOM model =================== + const navigate = useNavigate(); + const { state } = useLocation(); + const { mainJson, setMainJson, envJson, setEnvJson, activeScreen } = useMainJson(); + const { handleSessionExpiration } = useSessionManager(); + const [activeStep, setActiveStep] = useState(0); + const [skipped, setSkipped] = useState(new Set()); + const [isModalOpen, setIsModalOpen] = useState(false); + const windowSize = useRef([window.innerWidth, window.innerHeight]); + const configId = state?.configData.id; + const [loading, setLoading] = useState(false); + // END of DOM Model================ + + + const fetchConfigDetailsFromApi = async () => { + if (isTokenExpired()) { + handleSessionExpiration(location, null); + return; + } + + setLoading(true); + try { + const data = await callAPI(`api/sade_task/${configId}`, 'GET', null, 'JSON'); + console.log('Raw environment data:', data.environment); + + + const droneData = mapDroneData(data.drones); + mainJson.drones = droneData; + setMainJson(mainJson); + + const envData = mapEnvironmentData(data.environment); + setEnvJson(envData); + + setLoading(false); + } catch (error) { + console.error('Failed to fetch config details:', error); + setLoading(false); + } + }; + + const fetchConfigDetailsFromLocalStorage = () => { + try { + const savedJson = localStorage.getItem('mainJson'); + if (configId || !savedJson) return; + const parsedJson = JSON.parse(savedJson); + console.log('Saved environment JSON:', parsedJson.environment); + + + const droneData = mapDroneData(parsedJson.drones); + mainJson.drones = droneData; + setMainJson(mainJson); + + const envData = mapEnvironmentData(parsedJson.environment); + setEnvJson(envData); + + setLoading(false); + } catch (error) { + console.error('Failed to fetch config details:', error); + setLoading(false); + } + }; + + + useEffect(() => { + if (configId) { + localStorage.removeItem('mainJson'); + fetchConfigDetailsFromApi(); + } else { + fetchConfigDetailsFromLocalStorage(); + } + }, [configId]); + + const redirectToHome = () => { + navigate('/'); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const handleTabChange = (event, newValue) => { + setActiveStep(newValue); + }; + + useEffect(() => { + console.log('envJson updated:', envJson); + }, [envJson]); + + + const handleNext = () => { + let newSkipped = skipped; + if (isStepSkipped(activeStep)) { + newSkipped = new Set(newSkipped.values()); + newSkipped.delete(activeStep); + } + setActiveStep((prevActiveStep) => prevActiveStep + 1); + setSkipped(newSkipped); + }; + + const handleBack = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1); + }; + + const handleFinish = () => { + setIsModalOpen(true); + }; + + const stepsComponent = [ + { + name: 'Environment Configuration', + id: 1, + comp: ( + + ), + }, + { + name: 'sUAS Configuration', + id: 2, + comp: ( + + ), + }, + { + name: 'Test Configuration', + id: 3, + comp: ( + + ), + }, + ]; + + if (loading) { + return ( + + + + ); + } + + return ( + + + + + + + + + + + {/* */} + +
{activeStep != steps.length && stepsComponent[activeStep].comp}
+
+ + + + + + + + + + + +
+ + {activeStep === steps.length ? ( + Redirect to dashboard //TODO + ) : ( + + + + Back + + + {activeStep === steps.length - 1 ? ( + + Finish + + ) : ( + + Next + + )} + + + )} + + +
+ ); +} diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx index 84353ef4f..cbca57c4d 100644 --- a/frontend/src/components/Dashboard.jsx +++ b/frontend/src/components/Dashboard.jsx @@ -56,7 +56,7 @@ export default function Dashboard(parameter) { const [open, setOpen] = React.useState(false); const [selectedImage, setSelectedImage] = React.useState(); const [htmlLink, setHtmlLink] = React.useState(); - const [voilation, setVoilation] = React.useState(false) + const [violation, setViolation] = React.useState(false) const handleOpen = (img) => { setOpen(true); @@ -78,7 +78,7 @@ export default function Dashboard(parameter) { let content_split = content.split(";"); if(content.includes(keyMatch) && content_split.length == 4) { if(keyMatch == "FAIL") { - setVoilation(true) + setViolation(true) } if(droneMap.get(content_split[2]) == null) { droneMap.set(content_split[2], [content_split[3]]) @@ -396,7 +396,7 @@ export default function Dashboard(parameter) {
- {voilation ? + {violation ? Warning Violation Detected : null} diff --git a/frontend/src/components/EnvironmentConfiguration.jsx b/frontend/src/components/EnvironmentConfiguration.jsx index 3e3dd89a2..709dbe2ec 100644 --- a/frontend/src/components/EnvironmentConfiguration.jsx +++ b/frontend/src/components/EnvironmentConfiguration.jsx @@ -31,8 +31,51 @@ import Checkbox from '@mui/material/Checkbox'; import { DeleteOutline } from '@mui/icons-material'; import Snackbar from '@mui/material/Snackbar'; import Alert from '@mui/material/Alert'; +import { EnvironmentModel } from '../model/EnvironmentModel'; + +const buildEnvironmentModel = (conf, baseModel) => { + const model = EnvironmentModel.getReactStateBasedUpdate(baseModel ?? new EnvironmentModel()); + + const originSource = conf?.Origin ?? conf?._Origin ?? {}; + const windSource = conf?.Wind ?? conf?._Wind ?? model.Wind; + + const latitude = + originSource.Latitude ?? originSource.latitude ?? model.getOriginLatitude() ?? 0; + const longitude = + originSource.Longitude ?? originSource.longitude ?? model.getOriginLongitude() ?? 0; + const height = originSource.Height ?? originSource.height ?? model.getOriginHeight() ?? 0; + const name = originSource.Name ?? originSource.name ?? model.getOriginName() ?? ''; + const radius = originSource.Radius ?? originSource.radius ?? model.getOriginRadius(); + + const originImage = originSource.image ?? model.getOriginImage(); + model.Origin = { + latitude: Number(latitude), + longitude: Number(longitude), + height: Number(height), + name, + radius: Number(radius), + image: originImage, + }; + + model.TimeOfDay = conf?.TimeOfDay ?? conf?._TimeOfDay ?? model.TimeOfDay; + model.time = conf?.time ?? conf?._time ?? model.time; + model.enableFuzzy = Boolean(conf?.enableFuzzy ?? conf?._enableFuzzy ?? model.enableFuzzy); + model.timeOfDayFuzzy = Boolean( + conf?.timeOfDayFuzzy ?? conf?._timeOfDayFuzzy ?? model.timeOfDayFuzzy + ); + model.positionFuzzy = Boolean( + conf?.positionFuzzy ?? conf?._positionFuzzy ?? model.positionFuzzy + ); + model.windFuzzy = Boolean(conf?.windFuzzy ?? conf?._windFuzzy ?? model.windFuzzy); + model.UseGeo = conf?.UseGeo ?? conf?._UseGeo ?? model.UseGeo; + model.Wind = windSource; + + return model; +}; + export default function EnvironmentConfiguration (env) { + console.log('EnvironmentConfiguration props', env); const [backendInfo, setBackendInfo] = useState({ numQueuedTasks: 0, backendStatus: 'idle' @@ -82,11 +125,13 @@ export default function EnvironmentConfiguration (env) { Origin: { Latitude: 41.980381, Longitude: -87.934524, + Height: 2, }, TimeOfDay: "10:00:00", UseGeo: true, time:dayjs('2020-01-01 10:00') }); + const handleChangeSwitch = (val) => { setEnvConf(prevState => ({ ...prevState, @@ -96,10 +141,22 @@ export default function EnvironmentConfiguration (env) { const environmentJson = (event) => { env.environmentJson(event, env.id); } - //new added + React.useEffect(() => { - environmentJson(envConf) - }, [envConf]) + //console.log("Passing data up", envConf); + const model = buildEnvironmentModel(envConf, env.environmentJSON); + //console.log('Model origin lat/long', model.getOriginLatitude?.(), model.getOriginLongitude?.(), model._Origin); + + if (env.environmentJSONSetState){ + env.environmentJSONSetState(model, env.id); + } + + if (env.environmentJson){ + env.environmentJson(envConf, env.id); + } + + }, [envConf, env.environmentJSON]); + const Direction = [ {value:'N', id:5}, @@ -167,9 +224,10 @@ export default function EnvironmentConfiguration (env) { setEnvConf(prevState => ({ ...prevState, time: val, - TimeOfDay: val.$H + ':' + val.$m + ':' + val.$s + TimeOfDay: dayjs(val).format('HH:mm:ss') })) } + /* const handleWindChange = (val) => { setEnvConf(prevState => ({ ...prevState, @@ -179,6 +237,34 @@ export default function EnvironmentConfiguration (env) { } })) } + */ + const handleWindChange = (e) => { + const v = e.target.value; + + + if (v === '') { + setEnvConf(prev => ({ + ...prev, + Wind: { ...prev.Wind, Force: '' }, + })); + return; + } + + const n = Number(v); + if (Number.isNaN(n)) { + return; + } + + + const clamped = Math.min(50, Math.max(0, n)); + setEnvConf(prev => ({ + ...prev, + Wind: { ...prev.Wind, Force: clamped }, + })); + }; + + + const handleOriginChange = (val) => { setEnvConf(prevState => ({ ...prevState, @@ -235,6 +321,36 @@ export default function EnvironmentConfiguration (env) { }; */} + const handleWindTypeChange = (event) => { + setFuzzyAlert(false); + handleSnackBarVisibility(true); + const newWindType = event.target.value; + setSelectedWindType(newWindType); + + setEnvConf((prevState) => ({ + ...prevState, + Wind: { + ...prevState.Wind, + Type: newWindType, + Fluctuation: newWindType === "Turbulent Wind" ? fluctuationPercentage : 0, + }, + })); + }; + + const handleFLuctuationChange = (event) => { + const newFlucValue = event.target.value; + setSelectedFluctuationValue(newFlucValue); + + setEnvConf((prevState) => ({ + ...prevState, + Wind: { + ...prevState.Wind, + Fluctuation: newFlucValue, + }, + })); + }; + + /* const handleFLuctuationChange = (event) => { const newFlucValue = event.target.value; setSelectedFluctuationValue(newFlucValue); @@ -253,9 +369,10 @@ export default function EnvironmentConfiguration (env) { Type: newWindType, }, })); - */} + } }; - + + */ const handleDirection = (val) => { setEnvConf(prevState => ({ ...prevState, @@ -381,7 +498,7 @@ export default function EnvironmentConfiguration (env) { fluctuationPercentage: 0, }); }; - +/* const handleShearWindDirection = (e, id) => { console.log(id) const newArry = windShears.map((shear, index) => { @@ -425,9 +542,82 @@ const handleShearfluctuationPercentageChange = (e, id) => { } }) setwindShears(newArry) -} +} */ +const handleShearWindDirection = (e, index) => { + const newArry = windShears.map((shear, i) => { + if (i === index) { + return { + ...shear, + windDirection: e, + }; + } + return shear; + }); + setwindShears(newArry); + + setEnvConf((prevState) => ({ + ...prevState, + Wind: { + ...prevState.Wind, + [`Wind${index + 1}`]: { + ...prevState.Wind[`Wind${index + 1}`], + Direction: e, + }, + }, + })); + }; + + const handleShearWindChange = (e, index) => { + const newArry = windShears.map((shear, i) => { + if (i === index) { + return { + ...shear, + windVelocity: e, + }; + } + return shear; + }); + setwindShears(newArry); + + setEnvConf((prevState) => ({ + ...prevState, + Wind: { + ...prevState.Wind, + [`Wind${index + 1}`]: { + ...prevState.Wind[`Wind${index + 1}`], + Force: e, + }, + }, + })); + }; + + const handleShearfluctuationPercentageChange = (e, index) => { + const newArry = windShears.map((shear, i) => { + if (i === index) { + return { + ...shear, + fluctuationPercentage: e, + }; + } + return shear; + }); + setwindShears(newArry); + + setEnvConf((prevState) => ({ + ...prevState, + Wind: { + ...prevState.Wind, + [`Wind${index + 1}`]: { + ...prevState.Wind[`Wind${index + 1}`], + Fluctuation: e, + }, + }, + })); + }; + // Function to add a new wind shear entry for window + /* const addNewWindShear = () => { const newWindShearEntry = { windDirection: "", @@ -436,11 +626,32 @@ const handleShearfluctuationPercentageChange = (e, id) => { }; setwindShears([...windShears, newWindShearEntry]); }; - +*/ const [snackBarState, setSnackBarState] = React.useState({ open: false, }); + const addNewWindShear = () => { + const newWindShearEntry = { + windDirection: "", + windVelocity: 0, + fluctuationPercentage: 0, + }; + setwindShears([...windShears, newWindShearEntry]); + + setEnvConf((prevState) => ({ + ...prevState, + Wind: { + ...prevState.Wind, + [`Wind${windShears.length + 1}`]: { + Type: "Wind Shear", + Direction: "", + Force: 0, + Fluctuation: 0, + }, + }, + })); + }; const handleSnackBarVisibility = (val) => { @@ -461,14 +672,14 @@ const handleSnackBarVisibility = (val) => { {fuzzyAlert ? "Fuzzy Testing Changes is under development !" : "Wind Type Changes is under Developement !"} - + {/* */} - + {/* Wind */} - + Wind Type @@ -523,20 +734,21 @@ const handleSnackBarVisibility = (val) => { {/* {selectedWindType !== "Wind Shear" && ( */} - + + inputProps={{ min: 0, max: 50, }} + helperText={`Allowed range: 0-50 m/s`}/> {/* )} */} {(selectedWindType === "Turbulent Wind" || selectedWindType === "Wind Shear") && ( - + { */} {selectedWindType === "Wind Shear" && windShears.map((shear, index) => (( - - + + Wind Direction @@ -708,16 +920,21 @@ const handleSnackBarVisibility = (val) => { - + - + + + + + {/**/} {/* { */} - + - + { ) -} \ No newline at end of file +} diff --git a/frontend/src/components/FuzzyDashboard.jsx b/frontend/src/components/FuzzyDashboard.jsx index 1d9491153..a921bd23b 100644 --- a/frontend/src/components/FuzzyDashboard.jsx +++ b/frontend/src/components/FuzzyDashboard.jsx @@ -24,6 +24,7 @@ import AlertTitle from '@mui/material/AlertTitle'; import { wait } from '@testing-library/user-event/dist/utils'; import Snackbar from '@mui/material/Snackbar'; import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Link from '@mui/material/Link' const style = { position: 'absolute', @@ -37,15 +38,16 @@ const style = { p: 4, }; -export default function FuzzyDashboard(parameter) { +export default function FuzzyDashboard() { const navigate = useNavigate(); const location = useLocation(); - const deviation = location.state != null ? location.state.mainJson.monitors != null ? - location.state.mainJson.monitors.circular_deviation_monitor != null ? location.state.mainJson.monitors.circular_deviation_monitor.param[0] : null : null : null - const horizontal = location.state != null ? location.state.mainJson.monitors != null ? - location.state.mainJson.monitors.min_sep_dist_monitor != null ? location.state.mainJson.monitors.min_sep_dist_monitor.param[0] : null : null : null - const lateral = location.state != null ? location.state.mainJson.monitors != null ? - location.state.mainJson.monitors.min_sep_dist_monitor != null ? location.state.mainJson.monitors.min_sep_dist_monitor.param[1] : null : null : null + const resp = location.state.data + const deviation = location.state != null ? location.state.mainJson != null ? location.state.mainJson.monitors != null ? + location.state.mainJson.monitors.circular_deviation_monitor != null ? location.state.mainJson.monitors.circular_deviation_monitor.param[0] : null : null : null : null + const horizontal = location.state != null ? location.state.mainJson != null ? location.state.mainJson.monitors != null ? + location.state.mainJson.monitors.min_sep_dist_monitor != null ? location.state.mainJson.monitors.min_sep_dist_monitor.param[0] : null : null : null : null + const lateral = location.state != null ? location.state.mainJson != null ? location.state.mainJson.monitors != null ? + location.state.mainJson.monitors.min_sep_dist_monitor != null ? location.state.mainJson.monitors.min_sep_dist_monitor.param[1] : null : null : null : null const [fileArray, setFileArray] = React.useState([]) const [CircularDeviationMonitor, setCircularDeviationMonitor] = React.useState([]) const [CollisionMonitor, setCollisionMonitor] = React.useState([]) @@ -58,11 +60,12 @@ export default function FuzzyDashboard(parameter) { const [open, setOpen] = React.useState(false); const [selectedImage, setSelectedImage] = React.useState(); const [htmlLink, setHtmlLink] = React.useState(); - const [voilation, setVoilation] = React.useState(false) - const [isFuzzyList, setIsFuzzyList] = React.useState(false); + const [violation, setViolation] = React.useState(location.state.file.fail > 0 ? true : false) + const [isFuzzyList, setIsFuzzyList] = React.useState(location.state.file.fuzzy); const [fuzzyTest, setFuzzyTest] = React.useState([]); + const [fileName, setFileName] = React.useState(location.state.file.fileName) - const names = [{name:0},{name:5},{name:10}] + const names = [{name:0},{name:7},{name:14}] const handleOpen = (img) => { setOpen(true); @@ -73,48 +76,45 @@ export default function FuzzyDashboard(parameter) { setOpen(false) }; - const redirectToHome = () => { - navigate('/') + const redirectToReportDashboard = () => { + navigate('/report-dashboard') } - - const getInfoContents = (fileContents, keyMatch, droneMap) => { - const content_array = fileContents.split("\n"); - let infoContent = []; - content_array.map(content => { - let content_split = content.split(";"); - if(content.includes(keyMatch) && content_split.length == 4) { - if(keyMatch == "FAIL") { - setVoilation(true) - } - if(droneMap.get(content_split[2]) == null) { - droneMap.set(content_split[2], [content_split[3]]) - } else { - let val = [] - val = droneMap.get(content_split[2]) - val = val.concat([content_split[3]]) - droneMap.set(content_split[2], val) - } + // const getInfoContents = (fileContents, keyMatch, droneMap) => { + // const content_array = fileContents.split("\n"); + // let infoContent = []; + // content_array.map(content => { + // let content_split = content.split(";"); + // if(content.includes(keyMatch) && content_split.length == 4) { + // if(keyMatch == "FAIL") { + // setViolation(true) + // } + // if(droneMap.get(content_split[2]) == null) { + // droneMap.set(content_split[2], [content_split[3]]) + // } else { + // let val = [] + // val = droneMap.get(content_split[2]) + // val = val.concat([content_split[3]]) + // droneMap.set(content_split[2], val) + // } - infoContent.push(content_split[2] + ": " + content_split[3]) - } - }) - return droneMap; - } + // infoContent.push(content_split[2] + ": " + content_split[3]) + // } + // }) + // return droneMap; + // } const returnContentsItem = (colorCode, keyValue, info, icon, fuzzyValue, severity_val) => { - for (const mapKey of info.keys()) { + for (const mapKey of Object.keys(info)) { console.log(mapKey); - console.log(info.get(mapKey)) + console.log(info[mapKey]) let fuzzyValueArray = fuzzyValue.split("_") return (

{mapKey}

   - {/*
( Fuzzed Parameter : Wind Velocity = {fuzzyValueArray[2]} meters/s)
*/}
- {info.get(mapKey).map((val, id) => { + {info[mapKey].map((val, id) => { return ( - {/* */} @@ -132,396 +132,473 @@ export default function FuzzyDashboard(parameter) { ) } } - const handleDirectorySelectFuzzy = () => { - wait(1000); - console.log('fuzztTestNames----- in handleDirectorySelectFuzzy', names) + // const handleDirectorySelectFuzzy = () => { + // wait(1000); + // console.log('fuzztTestNames----- in handleDirectorySelectFuzzy', names) - setFuzzyTest([]); - names.map(id=> { - let unordered=[]; - let circular = []; - let collision = []; - let landscape = []; - let orderWay = []; - let pointDev = []; - let minSep = []; - let nonFly = []; - UnorderedWaypointMonitor.map(unorder => { - console.log('UnorderedWaypointMonitor---', UnorderedWaypointMonitor) - console.log('un unorder.fuzzyValue', unorder.fuzzyValue) - if(id.name == unorder.fuzzyValue) { - console.log('inside unod') - unordered.push(unorder) - } - console.log('unordered----', unordered) - }) - CircularDeviationMonitor.map(circularDev => { - if(id.name == circularDev.fuzzyValue) { - circular.push(circularDev); - } - }) - CollisionMonitor.map(coll => { - console.log('CollisionMonitor---', CollisionMonitor) - if(id.name == coll.fuzzyValue) { - collision.push(coll); - } - }) - LandspaceMonitor.map(land => { - if(id.name == land.fuzzyValue) { - landscape.push(land); - } - }) - OrderedWaypointMonitor.map( order => { - if(id.name == order.fuzzyValue) { - orderWay.push(order); - } - }) - PointDeviationMonitor.map(point => { - if(id.name == point.fuzzyValue) { - pointDev.push(point); - } - }) - MinSepDistMonitor.map(min => { - if(id.name == min.fuzzyValue) { - minSep.push(min); - } - }) - NoFlyZoneMonitor.map(zone => { - if(id.name == zone.fuzzyVale) { - nonFly.push(zone) - } - }) - setFuzzyTest(prevState => [ - ...prevState, - { - "name":id.name, - "UnorderedWaypointMonitor": unordered, - "CircularDeviationMonitor": circular, - "CollisionMonitor" : collision, - "LandspaceMonitor": landscape, - "OrderedWaypointMonitor": orderWay, - "PointDeviationMonitor": pointDev, - "MinSepDistMonitor": minSep, - "NoFlyZoneMonitor":nonFly - } - ]) - }) - } + // setFuzzyTest([]); + // names.map(id=> { + // let unordered=[]; + // let circular = []; + // let collision = []; + // let landscape = []; + // let orderWay = []; + // let pointDev = []; + // let minSep = []; + // let nonFly = []; + // UnorderedWaypointMonitor.map(unorder => { + // console.log('UnorderedWaypointMonitor---', UnorderedWaypointMonitor) + // console.log('un unorder.fuzzyValue', unorder.fuzzyValue) + // if(id.name == unorder.fuzzyValue) { + // console.log('inside unod') + // unordered.push(unorder) + // } + // console.log('unordered----', unordered) + // }) + // CircularDeviationMonitor.map(circularDev => { + // if(id.name == circularDev.fuzzyValue) { + // circular.push(circularDev); + // } + // }) + // CollisionMonitor.map(coll => { + // console.log('CollisionMonitor---', CollisionMonitor) + // if(id.name == coll.fuzzyValue) { + // collision.push(coll); + // } + // }) + // LandspaceMonitor.map(land => { + // if(id.name == land.fuzzyValue) { + // landscape.push(land); + // } + // }) + // OrderedWaypointMonitor.map( order => { + // if(id.name == order.fuzzyValue) { + // orderWay.push(order); + // } + // }) + // PointDeviationMonitor.map(point => { + // if(id.name == point.fuzzyValue) { + // pointDev.push(point); + // } + // }) + // MinSepDistMonitor.map(min => { + // if(id.name == min.fuzzyValue) { + // minSep.push(min); + // } + // }) + // NoFlyZoneMonitor.map(zone => { + // if(id.name == zone.fuzzyVale) { + // nonFly.push(zone) + // } + // }) + // setFuzzyTest(prevState => [ + // ...prevState, + // { + // "name":id.name, + // "UnorderedWaypointMonitor": unordered, + // "CircularDeviationMonitor": circular, + // "CollisionMonitor" : collision, + // "LandspaceMonitor": landscape, + // "OrderedWaypointMonitor": orderWay, + // "PointDeviationMonitor": pointDev, + // "MinSepDistMonitor": minSep, + // "NoFlyZoneMonitor":nonFly + // } + // ]) + // }) + // } + useEffect(() => { - handleDirectorySelectFuzzy() - }, - [UnorderedWaypointMonitor, CollisionMonitor, CircularDeviationMonitor, LandspaceMonitor, OrderedWaypointMonitor, PointDeviationMonitor, MinSepDistMonitor] - ) - - React.useEffect(() => {}, [fileArray]) - const handleDirectorySelect = (event) => { - const files = event.target.files; - let name = []; - for (let i = 0; i < files.length; i++) { - const fileReader = new FileReader(); - const file = files[i]; - console.log('file----', file) - const data = [...fileArray] - let path = file.webkitRelativePath - let fuzzyPathValue = null - let paths = path.split("/") - console.log('paths----', paths) - if(paths.length > 1) { - fuzzyPathValue = paths[1] - setIsFuzzyList(fuzzyPathValue.includes('Fuzzy')? true : false); - } - let fuzzyValueArray = fuzzyPathValue.split("_") - let exist = false - name.map(testName => { - if(testName.name == fuzzyValueArray[2]) { - exist = true; - } - }) - if(!exist) { - name.push({name:fuzzyValueArray[2]}) - } - console.log('fuzzyPathValue---', fuzzyPathValue) - if (file.type === 'text/plain') { - fileReader.onload = () => { - const fileContents = fileReader.result; - if(file.webkitRelativePath.includes("UnorderedWaypointMonitor")) { - setUnorderedWaypointMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - content:fileContents, - infoContent:getInfoContents(fileContents, "INFO", new Map()), - passContent:getInfoContents(fileContents, "PASS", new Map()), - failContent:getInfoContents(fileContents, "FAIL", new Map()), - fuzzyPath:fuzzyPathValue, - fuzzyValue: fuzzyValueArray[2] - } - ]) - } - if(file.webkitRelativePath.includes("CircularDeviationMonitor")) { - let info = getInfoContents(fileContents); - console.log('info----', info) - setCircularDeviationMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - content:fileContents, - infoContent:getInfoContents(fileContents, "INFO", new Map()), - passContent:getInfoContents(fileContents, "PASS", new Map()), - failContent:getInfoContents(fileContents, "FAIL", new Map()), - fuzzyPath:fuzzyPathValue, - fuzzyValue: fuzzyValueArray[2] - } - ]) + if(isFuzzyList) { + names.map(id=> { + let unordered=[]; + let circular = []; + let collision = []; + let landscape = []; + let orderWay = []; + let pointDev = []; + let minSep = []; + let nonFly = []; + resp.UnorderedWaypointMonitor.map(unorder => { + if(id.name == unorder.fuzzyValue) { + unordered.push(unorder) } - if(file.webkitRelativePath.includes("CollisionMonitor")) { - setCollisionMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - content:fileContents, - infoContent:getInfoContents(fileContents, "INFO", new Map()), - passContent:getInfoContents(fileContents, "PASS", new Map()), - failContent:getInfoContents(fileContents, "FAIL", new Map()), - fuzzyPath:fuzzyPathValue, - fuzzyValue: fuzzyValueArray[2] - } - ]) + }) + resp.CircularDeviationMonitor.map(circularDev => { + if(id.name == circularDev.fuzzyValue) { + circular.push(circularDev); } - if(file.webkitRelativePath.includes("LandspaceMonitor")) { - setLandspaceMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - content:fileContents, - infoContent:getInfoContents(fileContents, "INFO", new Map()), - passContent:getInfoContents(fileContents, "PASS", new Map()), - failContent:getInfoContents(fileContents, "FAIL", new Map()), - fuzzyPath:fuzzyPathValue, - fuzzyValue: fuzzyValueArray[2] - } - ]) + }) + resp.CollisionMonitor.map(coll => { + if(id.name == coll.fuzzyValue) { + collision.push(coll); } - if(file.webkitRelativePath.includes("OrderedWaypointMonitor")) { - setOrderedWaypointMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - content:fileContents, - infoContent:getInfoContents(fileContents, "INFO", new Map()), - passContent:getInfoContents(fileContents, "PASS", new Map()), - failContent:getInfoContents(fileContents, "FAIL", new Map()), - fuzzyPath:fuzzyPathValue, - fuzzyValue: fuzzyValueArray[2] - } - ]) + }) + resp.LandspaceMonitor.map(land => { + if(id.name == land.fuzzyValue) { + landscape.push(land); } - if(file.webkitRelativePath.includes("PointDeviationMonitor")) { - setPointDeviationMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - content:fileContents, - infoContent:getInfoContents(fileContents, "INFO", new Map()), - passContent:getInfoContents(fileContents, "PASS", new Map()), - failContent:getInfoContents(fileContents, "FAIL", new Map()), - fuzzyPath:fuzzyPathValue, - fuzzyValue: fuzzyValueArray[2] - } - ]) + }) + resp.OrderedWaypointMonitor.map( order => { + if(id.name == order.fuzzyValue) { + orderWay.push(order); } - if(file.webkitRelativePath.includes("MinSepDistMonitor")) { - setMinSepDistMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - content:fileContents, - infoContent:getInfoContents(fileContents, "INFO", new Map()), - passContent:getInfoContents(fileContents, "PASS", new Map()), - failContent:getInfoContents(fileContents, "FAIL", new Map()), - fuzzyPath:fuzzyPathValue, - fuzzyValue: fuzzyValueArray[2] - } - ]) + }) + resp.PointDeviationMonitor.map(point => { + if(id.name == point.fuzzyValue) { + pointDev.push(point); } - if(file.webkitRelativePath.includes("NoFlyZoneMonitor")) { - setNoFlyZoneMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - content:fileContents, - infoContent:getInfoContents(fileContents, "INFO", new Map()), - passContent:getInfoContents(fileContents, "PASS", new Map()), - failContent:getInfoContents(fileContents, "FAIL", new Map()), - fuzzyPath:fuzzyPathValue, - fuzzyValue: fuzzyValueArray[2] - } - ]) - } - - }; - fileReader.readAsText(file); - } else if (file.type === 'image/png') { - console.log("its image") - fileReader.onload = () => { - const fileContents = fileReader.result; - if(file.webkitRelativePath.includes("UnorderedWaypointMonitor")) { - let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") - setUnorderedWaypointMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - imgContent:URL.createObjectURL(file), - path:htmlfile, - fuzzyValue: fuzzyValueArray[2] - } - ]) + }) + resp.MinSepDistMonitor.map(min => { + if(id.name == min.fuzzyValue) { + minSep.push(min); } - if(file.webkitRelativePath.includes("CircularDeviationMonitor")) { - let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") - setCircularDeviationMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - imgContent:URL.createObjectURL(file), - path:htmlfile, - fuzzyValue: fuzzyValueArray[2] - } - ]) + }) + resp.NoFlyZoneMonitor.map(zone => { + if(id.name == zone.fuzzyVale) { + nonFly.push(zone) } - if(file.webkitRelativePath.includes("CollisionMonitor")) { - let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") - setCollisionMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - imgContent:URL.createObjectURL(file), - path:htmlfile, - fuzzyValue: fuzzyValueArray[2] - } - ]) + }) + setFuzzyTest(prevState => [ + ...prevState, + { + "name":id.name, + "UnorderedWaypointMonitor": unordered, + "CircularDeviationMonitor": circular, + "CollisionMonitor" : collision, + "LandspaceMonitor": landscape, + "OrderedWaypointMonitor": orderWay, + "PointDeviationMonitor": pointDev, + "MinSepDistMonitor": minSep, + "NoFlyZoneMonitor":nonFly } - if(file.webkitRelativePath.includes("LandspaceMonitor")) { - let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") - setLandspaceMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - imgContent:URL.createObjectURL(file), - path:htmlfile, - fuzzyValue: fuzzyValueArray[2] - } - ]) - } - if(file.webkitRelativePath.includes("OrderedWaypointMonitor")) { - let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") - setOrderedWaypointMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - imgContent:URL.createObjectURL(file), - path:htmlfile, - fuzzyValue: fuzzyValueArray[2] - } - ]) - } - if(file.webkitRelativePath.includes("PointDeviationMonitor")) { - let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") - setPointDeviationMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - imgContent:URL.createObjectURL(file), - path:htmlfile, - fuzzyValue: fuzzyValueArray[2] - } - ]) - } - if(file.webkitRelativePath.includes("MinSepDistMonitor")) { - let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") - setMinSepDistMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - imgContent:URL.createObjectURL(file), - path:htmlfile, - fuzzyValue: fuzzyValueArray[2] - } - ]) - } - if(file.webkitRelativePath.includes("NoFlyZoneMonitor")) { - let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") - setNoFlyZoneMonitor(prevState => [ - ...prevState, - { - name:file.name, - type:file.type, - imgContent:URL.createObjectURL(file), - path:htmlfile, - fuzzyValue: fuzzyValueArray[2] - } - ]) - } - } - fileReader.readAsText(file); - } else if (file.type === '') { - const directoryReader = new FileReader(); - directoryReader.onload = () => { - handleDirectorySelect({target: {files: directoryReader.result}}); - }; - directoryReader.readAsArrayBuffer(file); - } + ]) + }) + } + if(!isFuzzyList) { + setCircularDeviationMonitor(resp.CircularDeviationMonitor) + setCollisionMonitor(resp.CollisionMonitor) + setLandspaceMonitor(resp.LandspaceMonitor) + setMinSepDistMonitor(resp.MinSepDistMonitor) + setNoFlyZoneMonitor(resp.NoFlyZoneMonitor) + setOrderedWaypointMonitor(resp.OrderedWaypointMonitor) + setPointDeviationMonitor(resp.PointDeviationMonitor) + setUnorderedWaypointMonitor(resp.UnorderedWaypointMonitor) } - // setTestNames([name]); - console.log('name----', name) - // handleDirectorySelectFuzzy(name); - } + // handleDirectorySelectFuzzy() + }, + //[UnorderedWaypointMonitor, CollisionMonitor, CircularDeviationMonitor, LandspaceMonitor, OrderedWaypointMonitor, PointDeviationMonitor, MinSepDistMonitor] + [isFuzzyList]) + + // React.useEffect(() => {}, [fileArray]) + // const handleDirectorySelect = (event) => { + // const files = event.target.files; + // let name = []; + // for (let i = 0; i < files.length; i++) { + // const fileReader = new FileReader(); + // const file = files[i]; + // console.log('file----', file) + // const data = [...fileArray] + // let path = file.webkitRelativePath + // let fuzzyPathValue = null + // let paths = path.split("/") + // console.log('paths----', paths) + // if(paths.length > 1) { + // fuzzyPathValue = paths[1] + // setIsFuzzyList(fuzzyPathValue.includes('Fuzzy')? true : false); + // } + // let fuzzyValueArray = fuzzyPathValue.split("_") + // let exist = false + // name.map(testName => { + // if(testName.name == fuzzyValueArray[2]) { + // exist = true; + // } + // }) + // if(!exist) { + // name.push({name:fuzzyValueArray[2]}) + // } + // console.log('fuzzyPathValue---', fuzzyPathValue) + // if (file.type === 'text/plain') { + // fileReader.onload = () => { + // const fileContents = fileReader.result; + // if(file.webkitRelativePath.includes("UnorderedWaypointMonitor")) { + // setUnorderedWaypointMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // content:fileContents, + // infoContent:getInfoContents(fileContents, "INFO", new Map()), + // passContent:getInfoContents(fileContents, "PASS", new Map()), + // failContent:getInfoContents(fileContents, "FAIL", new Map()), + // fuzzyPath:fuzzyPathValue, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("CircularDeviationMonitor")) { + // let info = getInfoContents(fileContents); + // console.log('info----', info) + // setCircularDeviationMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // content:fileContents, + // infoContent:getInfoContents(fileContents, "INFO", new Map()), + // passContent:getInfoContents(fileContents, "PASS", new Map()), + // failContent:getInfoContents(fileContents, "FAIL", new Map()), + // fuzzyPath:fuzzyPathValue, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("CollisionMonitor")) { + // setCollisionMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // content:fileContents, + // infoContent:getInfoContents(fileContents, "INFO", new Map()), + // passContent:getInfoContents(fileContents, "PASS", new Map()), + // failContent:getInfoContents(fileContents, "FAIL", new Map()), + // fuzzyPath:fuzzyPathValue, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("LandspaceMonitor")) { + // setLandspaceMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // content:fileContents, + // infoContent:getInfoContents(fileContents, "INFO", new Map()), + // passContent:getInfoContents(fileContents, "PASS", new Map()), + // failContent:getInfoContents(fileContents, "FAIL", new Map()), + // fuzzyPath:fuzzyPathValue, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("OrderedWaypointMonitor")) { + // setOrderedWaypointMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // content:fileContents, + // infoContent:getInfoContents(fileContents, "INFO", new Map()), + // passContent:getInfoContents(fileContents, "PASS", new Map()), + // failContent:getInfoContents(fileContents, "FAIL", new Map()), + // fuzzyPath:fuzzyPathValue, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("PointDeviationMonitor")) { + // setPointDeviationMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // content:fileContents, + // infoContent:getInfoContents(fileContents, "INFO", new Map()), + // passContent:getInfoContents(fileContents, "PASS", new Map()), + // failContent:getInfoContents(fileContents, "FAIL", new Map()), + // fuzzyPath:fuzzyPathValue, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("MinSepDistMonitor")) { + // setMinSepDistMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // content:fileContents, + // infoContent:getInfoContents(fileContents, "INFO", new Map()), + // passContent:getInfoContents(fileContents, "PASS", new Map()), + // failContent:getInfoContents(fileContents, "FAIL", new Map()), + // fuzzyPath:fuzzyPathValue, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("NoFlyZoneMonitor")) { + // setNoFlyZoneMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // content:fileContents, + // infoContent:getInfoContents(fileContents, "INFO", new Map()), + // passContent:getInfoContents(fileContents, "PASS", new Map()), + // failContent:getInfoContents(fileContents, "FAIL", new Map()), + // fuzzyPath:fuzzyPathValue, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + + // }; + // fileReader.readAsText(file); + // } else if (file.type === 'image/png') { + // console.log("its image") + // fileReader.onload = () => { + // const fileContents = fileReader.result; + // if(file.webkitRelativePath.includes("UnorderedWaypointMonitor")) { + // let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") + // setUnorderedWaypointMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // imgContent:URL.createObjectURL(file), + // path:htmlfile, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("CircularDeviationMonitor")) { + // let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") + // setCircularDeviationMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // imgContent:URL.createObjectURL(file), + // path:htmlfile, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("CollisionMonitor")) { + // let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") + // setCollisionMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // imgContent:URL.createObjectURL(file), + // path:htmlfile, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("LandspaceMonitor")) { + // let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") + // setLandspaceMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // imgContent:URL.createObjectURL(file), + // path:htmlfile, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("OrderedWaypointMonitor")) { + // let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") + // setOrderedWaypointMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // imgContent:URL.createObjectURL(file), + // path:htmlfile, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("PointDeviationMonitor")) { + // let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") + // setPointDeviationMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // imgContent:URL.createObjectURL(file), + // path:htmlfile, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("MinSepDistMonitor")) { + // let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") + // setMinSepDistMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // imgContent:URL.createObjectURL(file), + // path:htmlfile, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // if(file.webkitRelativePath.includes("NoFlyZoneMonitor")) { + // let htmlfile = file.webkitRelativePath.replace("_plot.png", "_interactive.html") + // setNoFlyZoneMonitor(prevState => [ + // ...prevState, + // { + // name:file.name, + // type:file.type, + // imgContent:URL.createObjectURL(file), + // path:htmlfile, + // fuzzyValue: fuzzyValueArray[2] + // } + // ]) + // } + // } + // fileReader.readAsText(file); + // } else if (file.type === '') { + // const directoryReader = new FileReader(); + // directoryReader.onload = () => { + // handleDirectorySelect({target: {files: directoryReader.result}}); + // }; + // directoryReader.readAsArrayBuffer(file); + // } + // } + // // setTestNames([name]); + // console.log('name----', name) + // // handleDirectorySelectFuzzy(name); + // } const [folderDirectories, setFolderDirectories] = useState([]); - const componenet = () => { - const handleDirectorySelect = (selectedFiles) => { - console.log('Selected Files:', selectedFiles); - } + // const componenet = () => { + // const handleDirectorySelect = (selectedFiles) => { + // console.log('Selected Files:', selectedFiles); + // } - }; + // }; - const [snackBarState, setSnackBarState] = React.useState({ - open: false, - }); + // const [snackBarState, setSnackBarState] = React.useState({ + // open: false, + // }); - const handleSnackBarVisibility = (val) => { - setSnackBarState(prevState => ({ - ...prevState, - open: val - })) - } - const handleClickAway = () => { - // Prevent closing when clicking outside the box - handleSnackBarVisibility(true); - }; - useEffect(() => { - // Trigger the Snackbar to be open when the component mounts - handleSnackBarVisibility(true); - }, []); + // const handleSnackBarVisibility = (val) => { + // setSnackBarState(prevState => ({ + // ...prevState, + // open: val + // })) + // } + // const handleClickAway = () => { + // // Prevent closing when clicking outside the box + // handleSnackBarVisibility(true); + // }; + // useEffect(() => { + // // Trigger the Snackbar to be open when the component mounts + // handleSnackBarVisibility(true); + // }, []); return (
- {"Enhancements to the view test report details are in progress! As we are implementing new features, please use the “SELECT SIMULATION DATA DIRECTORY” button to select the files you wish to upload. These files should be located within the ‘Airsim’ folder under your Documents folder on your local machine. You can either upload the entire ‘reports’ folder to access all results or choose specific folders for each test you want to upload."} - + */} - - Acceptance Test Report - - + + {fileName} Detailed Report {/* */} {/*
UPLOAD FILE CONTENTS


*/} - + */}
- {voilation ? + + + + Back + + {/* Back */} + {/* */} + + + {violation ? Warning Violation Detected : null} @@ -592,7 +686,6 @@ export default function FuzzyDashboard(parameter) { {fuzzy.CollisionMonitor.map(function(file, index) { return (file.type=== 'text/plain' ? - {(returnContentsItem('darkgreen', index, file.passContent, , file.fuzzyPath, 'success'))} {(returnContentsItem('darkred', index, file.failContent, , file.fuzzyPath, 'error'))} @@ -609,7 +702,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -653,7 +746,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -699,7 +792,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -774,7 +867,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } @@ -803,7 +896,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -857,7 +950,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -887,7 +980,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -943,7 +1036,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -995,7 +1088,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -1041,7 +1134,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -1125,7 +1218,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } @@ -1167,7 +1260,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -1221,7 +1314,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -1251,7 +1344,7 @@ export default function FuzzyDashboard(parameter) { {/* */} + image={`data:image/png;base64 , ${file.imgContent}`}/> } ) @@ -1261,7 +1354,33 @@ export default function FuzzyDashboard(parameter) { } - + + + + Interactable HTMLs + +
    + {resp.htmlFiles && resp.htmlFiles.length > 0 ? ( + resp.htmlFiles.map((htmlFile, index) => ( +
  • + + {htmlFile.name} + +
  • + )) + ) : ( + + No HTML files available for interaction. + + )} +
+
+
{/* end of fuzzy */}
- + {/* Redirect to Html page */} diff --git a/frontend/src/components/HorizontalLinearStepper.jsx b/frontend/src/components/HorizontalLinearStepper.jsx index 432f10b6e..01bf36419 100644 --- a/frontend/src/components/HorizontalLinearStepper.jsx +++ b/frontend/src/components/HorizontalLinearStepper.jsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Box from '@mui/material/Box'; +import { Box, Grid } from '@mui/material'; import Stepper from '@mui/material/Stepper'; import Step from '@mui/material/Step'; import StepLabel from '@mui/material/StepLabel'; @@ -8,56 +8,59 @@ import Typography from '@mui/material/Typography'; import styled from '@emotion/styled'; import MissionConfiguration from './Configuration/MissionConfiguration'; import EnvironmentConfiguration from './EnvironmentConfiguration'; -import MonitorControl from './MonitorControl' +import MonitorControl from './MonitorControl'; import Home from '../pages/Home'; import { useNavigate } from 'react-router-dom'; import HomeIcon from '@mui/icons-material/Home'; import Tooltip from '@mui/material/Tooltip'; - - +import CesiumMap from './cesium/CesiumMap'; +import { mapControls } from '../constants/map'; +import ControlsDisplay from './Configuration/ControlsDisplay'; const StyledButton = styled(Button)` - border-radius: 25px;font-size: 18px; font-weight: bolder + border-radius: 25px; + font-size: 18px; + font-weight: bolder; `; -const steps = [ - 'Environment Configuration', - 'Mission Configuration', - 'Test Configuration' -]; +const steps = ['Environment Configuration', 'Mission Configuration', 'Test Configuration']; -export default function HorizontalLinearStepper(data) { - const navigate = useNavigate(); +export default function HorizontalLinearStepper(data) { + const navigate = useNavigate(); const [activeStep, setActiveStep] = React.useState(0); const [skipped, setSkipped] = React.useState(new Set()); - const [mainJson, setJson] = React.useState({ - Drones:null, + const [mainJson, setJson, activeScreen] = React.useState({ + Drones: null, environment: null, - monitors: null - }) + monitors: null, + }); const windowSize = React.useRef([window.innerWidth, window.innerHeight]); const redirectToHome = () => { - navigate('/') - } + navigate('/'); + }; const isStepSkipped = (step) => { return skipped.has(step); }; const setMainJson = (envJson, id) => { - if(id == "environment" && mainJson.Drones != null && mainJson.Drones[0].X != envJson.Origin.Latitude) { - setJson(prevState => ({ + if ( + id == 'environment' && + mainJson.Drones != null && + mainJson.Drones[0].X != envJson.Origin.Latitude + ) { + setJson((prevState) => ({ ...prevState, - Drones:null, - })) + Drones: null, + })); } - setJson(prevState => ({ + setJson((prevState) => ({ ...prevState, - [id]: envJson - })) - } + [id]: envJson, + })); + }; const handleNext = () => { let newSkipped = skipped; @@ -70,7 +73,7 @@ export default function HorizontalLinearStepper(data) { // } setActiveStep((prevActiveStep) => prevActiveStep + 1); setSkipped(newSkipped); - invokePostAPI(); + addTask(); }; const handleBack = () => { @@ -78,103 +81,183 @@ export default function HorizontalLinearStepper(data) { }; React.useEffect(() => { - if( mainJson.environment != null && mainJson.environment.enableFuzzy == true && mainJson.environment.enableFuzzy != null) { - setJson(prevState => ({ + if ( + mainJson.environment != null && + mainJson.environment.enableFuzzy == true && + mainJson.environment.enableFuzzy != null + ) { + setJson((prevState) => ({ ...prevState, FuzzyTest: { - target: "Wind", - precision: 5 - } - })) - delete mainJson.environment["enableFuzzy"] + target: 'Wind', + precision: 5, + }, + })); + delete mainJson.environment['enableFuzzy']; } - if (mainJson.environment != null && mainJson.environment.enableFuzzy == false && mainJson.FuzzyTest != null) { - delete mainJson.FuzzyTest + if ( + mainJson.environment != null && + mainJson.environment.enableFuzzy == false && + mainJson.FuzzyTest != null + ) { + delete mainJson.FuzzyTest; } - }, [mainJson]) - - - - const invokePostAPI = () => { - console.log("mainJson-----", mainJson) - if(activeStep === steps.length -1) { - mainJson.Drones.map(drone => { - - delete drone["id"] - delete drone["droneName"] - delete drone.Sensors.Barometer["Key"] - delete drone.Sensors.Magnetometer["Key"] - delete drone.Sensors.GPS["Key"] - // delete drone.Sensors.GPS["EphTimeConstant"] - // drone.Sensors.GPS["EpvTimeConstant"] ? delete drone.Sensors.GPS["EpvTimeConstant"]: null - // drone.Sensors.GPS["EphInitial"] ? delete drone.Sensors.GPS["EphInitial"]: null - // drone.Sensors.GPS["EpvInitial"] ? delete drone.Sensors.GPS["EpvInitial"]: null - // drone.Sensors.GPS["EphFinal"] ? delete drone.Sensors.GPS["EphFinal"]: null - // drone.Sensors.GPS["EpvFinal"] ? delete drone.Sensors.GPS["EpvFinal"]: null - // drone.Sensors.GPS["EphMin3d"] ? delete drone.Sensors.GPS["EphMin3d"]: null - // drone.Sensors.GPS["EphMin2d"] ? delete drone.Sensors.GPS["EphMin2d"]: null - // drone.Sensors.GPS["UpdateLatency"] ? delete drone.Sensors.GPS["UpdateLatency"]: null - // drone.Sensors.GPS["StartupDelay"] ? delete drone.Sensors.GPS["StartupDelay"]: null - // delete drone.Cameras.CaptureSettings.map(capt => { - // delete capt["key"] - // }) - }) - - delete mainJson.environment["time"] - mainJson.monitors.circular_deviation_monitor["enable"] == true ? delete mainJson.monitors.circular_deviation_monitor["enable"] : delete mainJson.monitors.circular_deviation_monitor - mainJson.monitors.collision_monitor["enable"] == true ? delete mainJson.monitors.collision_monitor["enable"] : delete mainJson.monitors.collision_monitor - mainJson.monitors.unordered_waypoint_monitor["enable"] == true ? delete mainJson.monitors.unordered_waypoint_monitor["enable"] : delete mainJson.monitors.unordered_waypoint_monitor - mainJson.monitors.ordered_waypoint_monitor["enable"] == true ? delete mainJson.monitors.ordered_waypoint_monitor["enable"] : delete mainJson.monitors.ordered_waypoint_monitor - mainJson.monitors.point_deviation_monitor["enable"] == true ? delete mainJson.monitors.point_deviation_monitor["enable"] : delete mainJson.monitors.point_deviation_monitor - mainJson.monitors.min_sep_dist_monitor["enable"] == true ? delete mainJson.monitors.min_sep_dist_monitor["enable"] : delete mainJson.monitors.min_sep_dist_monitor - mainJson.monitors.landspace_monitor["enable"] == true ? delete mainJson.monitors.landspace_monitor["enable"] : delete mainJson.monitors.landspace_monitor - mainJson.monitors.no_fly_zone_monitor["enable"] == true ? delete mainJson.monitors.no_fly_zone_monitor["enable"] : delete mainJson.monitors.no_fly_zone_monitor - mainJson.monitors.drift_monitor["enable"] == true ? delete mainJson.monitors.drift_monitor["enable"] : delete mainJson.monitors.drift_monitor - mainJson.monitors.battery_monitor["enable"] == true ? delete mainJson.monitors.battery_monitor["enable"] : delete mainJson.monitors.battery_monitor - delete mainJson.environment["enableFuzzy"] - delete mainJson.environment["timeOfDayFuzzy"] - delete mainJson.environment["windFuzzy"] - delete mainJson.environment["positionFuzzy"] - console.log('mainJson-----', JSON.stringify(mainJson)) - navigate('/report-dashboard', { - state: {mainJson: mainJson} - }) - fetch('http://127.0.0.1:5000/addTask', { - method: 'POST', - headers: { - 'Content-type': 'application/json', + }, [mainJson]); + + //Start Logic For Calling POST + + //This function goes in and gets the drone data from main JSON and formats it all pretty for the POST Call + function getDronesForPayload(mainJson) { + return Array.isArray(mainJson?.Drones) + ? mainJson.Drones.map((d) => { + const { id, droneName, Sensors, ...rest } = d || {}; + const sanitizedSensors = Sensors + ? { + ...Sensors, + Barometer: Sensors.Barometer + ? (({ Key, ...b }) => b)(Sensors.Barometer) + : undefined, + Magnetometer: Sensors.Magnetometer + ? (({ Key, ...m }) => m)(Sensors.Magnetometer) + : undefined, + GPS: Sensors.GPS + ? (({ Key, ...g }) => g)(Sensors.GPS) + : undefined, + } + : undefined; + return { ...rest, Sensors: sanitizedSensors }; + }) + : []; + } + + //this function goes in and gets the data for the environment from mainJSON + function getEnvironmentForPayload(env) { + if (!env) return null; + + const useGeo = !!env.UseGeo; + + const origin = env.Origin || {}; + const lat = origin.Latitude ?? origin.latitude; + const lon = origin.Longitude ?? origin.longitude; + + const environmentToSend = { + UseGeo: useGeo, + Origin: { + Latitude: lat, + Longitude: lon, }, - body: JSON.stringify(mainJson), - }) - .then(res => res.json()) - .then(res => console.log(res)); + }; + + if (env.Wind) environmentToSend.Wind = env.Wind; + if (env.TimeOfDay) environmentToSend.TimeOfDay = env.TimeOfDay; + if (env.Sades) environmentToSend.Sades = env.Sades; + + return environmentToSend; + } + + //meat and potatoes, this function actually makes the call + //the other end is simulation_server.py line 139 + async function addTask() { + if (activeStep !== steps.length - 1) return; + + const dronesToSend = getDronesForPayload(mainJson); + if (dronesToSend.length === 0) { + console.warn('No drones configured; not submitting.'); + return; } - } + + const environmentToSend = getEnvironmentForPayload(mainJson.environment); + if ( + !environmentToSend || + (environmentToSend.UseGeo && + (environmentToSend.Origin.Latitude == null || + environmentToSend.Origin.Longitude == null)) + ) { + console.warn('Environment incomplete; not submitting.'); + return; + } + + const payload = { + Drones: dronesToSend, + environment: environmentToSend, + ...(mainJson.monitors ? { monitors: mainJson.monitors } : {}), + ...(mainJson.FuzzyTest ? { FuzzyTest: mainJson.FuzzyTest } : {}), + }; + + try { + console.log('POST /addTask payload:', payload); + const res = await fetch('http://127.0.0.1:5000/addTask', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + const bodyText = await res.text(); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${bodyText}`); + + let data; + try { data = JSON.parse(bodyText); } catch { data = { raw: bodyText }; } + console.log('Task queued:', data); + + } catch (err) { + console.error('Submit failed:', err); + + } + } + const stepsComponent = [ { - name:'Environment Configuration', - id:1, - comp: + name: 'Environment Configuration', + id: 1, + comp: ( + + ), }, { - name:'Mission Configuration', - id:2, - comp: + name: 'Mission Configuration', + id: 2, + comp: ( + + ), }, { - name:'Test Configuration', - id:3, - comp: - } + name: 'Test Configuration', + id: 3, + comp: ( + + ), + }, ]; return ( - Requirement - + + Requirement + + + + + + {data.desc} - {data.desc} - + {steps.map((label, index) => { const stepProps = {}; const labelProps = {}; @@ -201,25 +284,42 @@ export default function HorizontalLinearStepper(data) { {/* Requirement {data.desc} */} - {stepsComponent.map((compo, index) => { - return ( - (compo.id) === (activeStep + 1) ? (compo.comp): '' - ) - })} - - - Back - - - - {activeStep === steps.length - 1 ? 'Finish' : 'Next'} - + + + {stepsComponent.map((compo, index) => { + return compo.id === activeStep + 1 ? compo.comp : ''; + })} + + + Back + + + + {activeStep === steps.length - 1 ? 'Finish' : 'Next'} + + + + + + + + + + + + + + )} diff --git a/frontend/src/components/Loading.jsx b/frontend/src/components/Loading.jsx new file mode 100644 index 000000000..d8966070f --- /dev/null +++ b/frontend/src/components/Loading.jsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import CircularProgress from '@mui/material/CircularProgress'; +import Box from '@mui/material/Box'; + +export default function Loading() { + return ( + + + + ); +} diff --git a/frontend/src/components/MonitorControl.jsx b/frontend/src/components/MonitorControl.jsx index 59ed49e5a..b5f7d2456 100644 --- a/frontend/src/components/MonitorControl.jsx +++ b/frontend/src/components/MonitorControl.jsx @@ -17,6 +17,7 @@ import MonitorTabels from './MonitorTabels'; import ButtonGroup from '@mui/material/ButtonGroup'; import Button from '@mui/material/Button'; import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; import EnvironmentConfiguration from './EnvironmentConfiguration'; import dayjs from 'dayjs'; @@ -91,7 +92,7 @@ export default function MonitorControl (monJson) { }, battery_monitor:{ enable:false, - param:[1] + param:[1,1] } }) @@ -122,14 +123,14 @@ export default function MonitorControl (monJson) { React.useEffect(() => { environmentJson(envConf) }, [envConf]) - const handleBatteruMonitor = (val) => { + const handleBatteryMonitor = (val, index) => { setMonitor(prevState => ({ ...prevState, battery_monitor: { - ...monitor.battery_monitor, - param: [ - parseFloat(val.target.value) - ] + ...prevState.battery_monitor, + param: prevState.battery_monitor.param.map((item, i) => + i === index ? parseFloat(val.target.value) : item + ) } })) } @@ -608,6 +609,46 @@ export default function MonitorControl (monJson) { tableData:null, isMultipleTable: false }, + { + name: "Battery", + value: '2.8', + description: "Test whether the drone's battery dropped below the certain percentage", + colorText:monitor.battery_monitor.enable == true ? 'green': null, + btns: + {monitor.battery_monitor.enable == true ? + + Configure the current battery capacity + + {/* Input for current battery capacity */} + + + handleBatteryMonitor(val, 0)} value={monitor.battery_monitor.param[0]}> + + + {/* Input for target failure battery percentage */} + + + handleBatteryMonitor(val, 1)} value={monitor.battery_monitor.param[1]}> + + + + + + Info + This feature is still in development. + + + + : null}, + images: null, + enableBtn: + Status    + { + handleChangeSwitch(e, "battery_monitor") + }} inputProps={{'aria-label': 'controlled'}} />} label={monitor.battery_monitor.enable ? "Enabled" : "Disabled"} />, + tableData: null, + isMultipleTable: false + } ] const handleFuzzyWindChange = (val) => { setEnvConf(prevState => ({ @@ -701,7 +742,7 @@ export default function MonitorControl (monJson) { onChange={handleVerticalChange} variant="scrollable" scrollButtons="auto" - sx={{ borderRight: 1, borderColor: 'divider' }} + sx={{ borderRight: 1, borderColor: 'divider', minWidth:'100px' }} > {singleMonitors.map(function(single, index) { return diff --git a/frontend/src/components/ReportDashboard.jsx b/frontend/src/components/ReportDashboard.jsx index efd9b9c0f..43da2471d 100644 --- a/frontend/src/components/ReportDashboard.jsx +++ b/frontend/src/components/ReportDashboard.jsx @@ -1,13 +1,13 @@ -import CardMedia from '@mui/material/CardMedia' +import CardMedia from '@mui/material/CardMedia'; import Box from '@mui/material/Box'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import Paper from '@mui/material/Paper'; import CheckIcon from '@mui/icons-material/Check'; -import List from '@mui/material/List' +import List from '@mui/material/List'; import ClearIcon from '@mui/icons-material/Clear'; import InfoIcon from '@mui/icons-material/Info'; -import { useLocation } from "react-router-dom"; +import { useLocation } from 'react-router-dom'; import { Container } from '@mui/material'; import Grid from '@mui/material/Grid'; import Button from '@mui/material/Button'; @@ -20,10 +20,10 @@ import Tooltip from '@mui/material/Tooltip'; import AlertTitle from '@mui/material/AlertTitle'; import { wait } from '@testing-library/user-event/dist/utils'; //import { Card, CardContent } from '@mui/material'; -import PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; //import FuzzyDashboard from '/dashboard'; -import { Card, CardContent, CardHeader, Typography } from '@mui/material'; -import FuzzyDashboard from './FuzzyDashboard'; +import { Card, CardContent, CardHeader, Typography } from '@mui/material'; +import FuzzyDashboard from './FuzzyDashboard'; import React, { useEffect } from 'react'; import { makeStyles } from '@mui/styles'; import Snackbar from '@mui/material/Snackbar'; @@ -31,36 +31,51 @@ import Snackbar from '@mui/material/Snackbar'; import Accordion from '@mui/material/Accordion'; import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionDetails from '@mui/material/AccordionDetails'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import CircularProgress from '@mui/material/CircularProgress'; +import CircularProgress from '@mui/material/CircularProgress'; import { Table, TableBody, TableCell, TableRow, TableColumn } from '@mui/material'; +import { Link } from 'react-router-dom'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import Loading from './Loading'; - - -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles((theme) => ({ lightBlueBackground: { - backgroundColor: '#e3f2fd', + backgroundColor: '#e3f2fd', + }, + cardMedia: { + width: '80%', + height: 'auto', + }, + fullWidthBox: { + width: '100vw', + margin: 0, + padding: 0, }, card: { maxWidth: 400, height: 270, - border: '1px solid lightgreen', - backgroundColor: '#e3f2fd', - boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2)', + border: '1px solid lightgreen', + backgroundColor: '#e3f2fd', + boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2)', + }, + fullScreenContainer: { + width: '50%', + padding: 0, + margin: 0, }, invalidData: { fontWeight: 'bold', - color: 'red', + color: 'red', }, + button: { - backgroundColor: '#1976d2', - color: '#fff', + backgroundColor: '#1976d2', + color: '#fff', '&:hover': { - backgroundColor: '#1565c0', + backgroundColor: '#1565c0', }, }, -})); +})); //const sampleData = [ // { @@ -71,258 +86,416 @@ const useStyles = makeStyles((theme) => ({ // "pass": 0 //}, //{ - // "contains_fuzzy": true, - // "drone_count": 1, - // "fail": 0, - // "filename": "2023-10-10-15-09-38_Batch_1", - // "pass": 2 +// "contains_fuzzy": true, +// "drone_count": 1, +// "fail": 0, +// "filename": "2023-10-10-15-09-38_Batch_1", +// "pass": 2 //}, //{ - // "contains_fuzzy": true, - // "drone_count": 10, - // "fail": 2, - // "filename": "2023-10-10-15-13-17_Batch_2", - // "pass": 8 +// "contains_fuzzy": true, +// "drone_count": 10, +// "fail": 2, +// "filename": "2023-10-10-15-13-17_Batch_2", +// "pass": 8 //}, //{ - // "contains_fuzzy": true, - // "drone_count": 15, - // "fail": 5, - // "filename": "2023-10-10-15-15-35_Batch_3", - // "pass": 10 +// "contains_fuzzy": true, +// "drone_count": 15, +// "fail": 5, +// "filename": "2023-10-10-15-15-35_Batch_3", +// "pass": 10 //} //]; - - export default function ReportDashboard(parameter) { +export default function ReportDashboard() { + const [reportFiles, setReportFiles] = React.useState([]); + const [isLoading, setIsloading] = React.useState(false); + // const isFuzzy = file.filename.includes('Fuzzy'); + const classes = useStyles(); + const location = useLocation(); - const [reportFiles, setReportFiles] = React.useState([]); - // const isFuzzy = file.filename.includes('Fuzzy'); - const classes = useStyles(); + const isReportDashboard = location.pathname.includes('/report-dashboard'); - const navigate = useNavigate(); - const redirectToHome = () => { - navigate('/') - } - const redirectToFuzzyDashboard = () => { - navigate('/dashboard') - } - - useEffect(() => { - const fetchData = () => { - fetch('http://localhost:5000/list-reports', { method: 'GET' }) - .then((res) => { - if (!res.ok) { - throw new Error('No response from server/something went wrong'); - } - return res.json(); - }) - .then((data) => { - // 'data.reports' containing filename and fuzzy info - console.log('Report Files:', data.reports); - setReportFiles(data.reports); - }) - .catch((error) => { - console.error('Error fetching report data:', error); - }); - }; - - // Set the sample data initially - //setReportFiles(sampleData); - - // Fetch data after setting the sample data - setReportFiles([]); + const navigate = useNavigate(); + const redirectToHome = () => { + navigate('/'); + }; + const redirectToFuzzyDashboard = () => { + navigate('/dashboard'); + }; - fetchData(); - }, []); + useEffect(() => { + const fetchData = () => { + setIsloading(true); + fetch('http://localhost:5000/list-reports', { method: 'GET' }) + .then((res) => { + if (!res.ok) { + throw new Error('No response from server/something went wrong'); + } + return res.json(); + }) + .then((data) => { + // 'data.reports' containing filename and fuzzy info + setReportFiles(data.reports ?? []); + }) + .catch((error) => { + console.error('Error fetching report data:', error); + }) + .finally(() => { + setIsloading(false); + }); + }; + fetchData(); + }, []); const [snackBarState, setSnackBarState] = React.useState({ open: false, }); const handleSnackBarVisibility = (val) => { - setSnackBarState(prevState => ({ + setSnackBarState((prevState) => ({ ...prevState, - open: val - })) - } + open: val, + })); + }; const handleClickAway = () => { handleSnackBarVisibility(true); }; useEffect(() => { handleSnackBarVisibility(true); }, []); - - - return ( - <> - {reportFiles.length === 0 && ( - <> - handleSnackBarVisibility(false)} - > - handleSnackBarVisibility(false)} - severity="info" - sx={{ maxHeight: '150px', maxWidth: '100%' }} - > - {"No reports found"} - - + const getFolderContents = (file) => { + fetch(`http://localhost:5000/list-folder-contents/${file.filename}`, { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: {}, + }) + .then((res) => { + if (!res.ok) { + throw new Error('No response from server/something went wrong'); + } + return res.json(); + }) + .then((data) => { + console.log('File Json: ', data); + navigate('/dashboard', { + state: { + data: data, + file: { fuzzy: file.contains_fuzzy, fileName: file.filename, fail: file.fail }, + }, + }); + return data; + }) + .catch((error) => { + console.error('Error fetching report data:', error); + }); + }; + const handleButtonClick = (file) => { + console.log('Button clicked:', file); + getFolderContents(file); + }; + + const acceptanceReportTypography = ( + + + Acceptance Report + + {isReportDashboard && ( + + + + )} + + ); - - Acceptance Report - - - - + return ( + <> + + + - - {/* ... (existing Container, Paper, and div) */} - + {acceptanceReportTypography} -
- -
- - )} + {isLoading ? ( + + ) : ( + <> + {reportFiles.length === 0 && ( + <> + handleSnackBarVisibility(false)} + > + handleSnackBarVisibility(false)} + severity='info' + sx={{ maxHeight: '150px', maxWidth: '100%' }} + > + {'No reports found'} + + - {reportFiles.length > 0 && ( - <> - - Acceptance Report - {/* ... */} - - - {reportFiles.map((file) => { - const parts = file.filename.split('_'); - const failed = file.fail > 0; - const passed = file.pass > 0; + + {/* ... (existing Container, Paper, and div) */} + - if (!file || !file.filename || file.filename.includes('.DS_Store')) { - return null; - } +
+ +
+ + )} + {reportFiles.length > 0 && ( + <> + + {reportFiles.map((file) => { + const parts = file.filename.split('_'); + const failed = file.fail > 0; + const passed = file.pass > 0; - if (parts.length < 2) { - return ( - - - {/* ... (other JSX components) */} - - - ); - } + if (!file || !file.filename || file.filename.includes('.DS_Store')) { + return null; + } - const datePart = parts[0]; - const batchName = parts.slice(1).join('_'); + if (parts.length < 2) { + return ( + + {/* ... (other JSX components) */} + + ); + } - const date = datePart.substr(0, 10); - const time = datePart.substr(11, 8); + const datePart = parts[0]; + const batchName = parts.slice(1).join('_'); - const formattedDate = `${date.substr(5, 2)}-${date.substr(8, 2)}-${date.substr(0, 4)}`; - const formattedTime = `${time.substr(0, 2)}:${time.substr(3, 2)}:${time.substr(6, 2)}`; + const date = datePart.substr(0, 10); + const time = datePart.substr(11, 8); - const formattedTimestamp = `${formattedDate} ${formattedTime}`; + const formattedDate = `${date.substr(5, 2)}-${date.substr(8, 2)}-${date.substr( + 0, + 4, + )}`; + const formattedTime = `${time.substr(0, 2)}:${time.substr(3, 2)}:${time.substr( + 6, + 2, + )}`; - const passedPercent = Math.round((file.pass / (file.pass + file.fail)) * 100); - - return ( - - - }> - - {/* Date and Batch Name */} - - - {formattedTimestamp} - {batchName} - - + const formattedTimestamp = `${formattedDate} ${formattedTime}`; - -
- {file.fail > 0 && ( -
-
- - -
-
- )} - {passed && ( -
-
- - -
-
- )} -
+ const passedPercent = Math.round((file.pass / (file.pass + file.fail)) * 100); + return ( + + + }> + + {/* Date and Batch Name */} + + + {formattedTimestamp} + + {batchName} + + {file.contains_fuzzy && ( + + )} + + - - -
- - - - - {/* */} - Drone Count - Pass - Fail - - - - {/* */} - {file.drone_count} - {file.pass} - {file.fail} - - -
- {file.contains_fuzzy && ( - -

Fuzzy Testing {file.contains_fuzzy}

-
- )} - {!file.contains_fuzzy && ( - -

Simulation Testing

-
- )} -
-
-
- ); - })} -
+ +
+ {file.fail > 0 && ( +
+
+ + + ❌ + +
+
+ )} + {passed && ( +
+
+ + + ✅ + +
+
+ )} +
+
+
+ + + + + + {/* */} + + Drone Count + + + Pass + + + Fail + + + + {/* */} + {file.drone_count} + {file.pass} + {file.fail} + + +
+
+ handleButtonClick(file)} + > + Simulation Data + +
+
+ + + ); + })} + + + )} + + )} - )} - -); -} \ No newline at end of file + ); +} + +ReportDashboard.propTypes = { + isHomePage: PropTypes.bool, +}; \ No newline at end of file diff --git a/frontend/src/components/cesium/CesiumMap.jsx b/frontend/src/components/cesium/CesiumMap.jsx new file mode 100644 index 000000000..715f1de45 --- /dev/null +++ b/frontend/src/components/cesium/CesiumMap.jsx @@ -0,0 +1,234 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { Viewer, CameraFlyTo, Cesium3DTileset, Entity } from 'resium'; +import { + Cartesian3, + IonResource, + Math as CesiumMath, + createWorldTerrainAsync, + sampleTerrainMostDetailed, + Ion, + Cartographic, +} from 'cesium'; +import PropTypes from 'prop-types'; +import DrawSadeZone from './DrawSadeZone'; +import DroneDragAndDrop from './DroneDragAndDrop'; +// import RadiusDragAndDrop from './RegionDragAndDrop'; +import TimeLineSetterCesiumComponent from './TimeLineSetterCesiumComponent'; +import { useMainJson } from '../../contexts/MainJsonContext'; +import { originTypes } from '../../constants/env'; +import { EnvironmentModel } from '../../model/EnvironmentModel'; + +const CesiumMap = ({ activeConfigStep }) => { + const DEFAULT_CAMERA_HEIGHT = 5000; + const { mainJson, envJson, setEnvJson, registerSetCameraByPosition } = useMainJson(); + console.log('envJson:', envJson); + console.log('Origin:', envJson.Origin); + const viewerRef = useRef(null); + const flyPending = useRef(false); + + const [viewerReady, setViewerReady] = useState(false); + const [cameraPosition, setCameraPosition] = useState({ + destination: Cartesian3.fromDegrees( + envJson.Origin.longitude, + envJson.Origin.latitude, + DEFAULT_CAMERA_HEIGHT, + ), + orientation: { + heading: CesiumMath.toRadians(10), + pitch: -Math.PI / 2, + }, + }); + const OSMBuildingsAssetId = 96188; + const google3DTilesAssetId = 2275207; + + Ion.defaultAccessToken = process.env.REACT_APP_CESIUM_ION_ACCESS_TOKEN; + + const setCameraByPosition = (position = null, pitch = null) => { + if (!viewerReady) return; + const viewer = viewerRef.current.cesiumElement; + const { camera } = viewer; + setCameraPosition({ + destination: position ?? camera.position, + orientation: { + heading: camera.heading, + pitch: pitch ?? camera.pitch, + }, + }); + }; + + const setCameraByLongLat = (long, lat, altitude, pitch) => { + if (!viewerReady) return; + const viewer = viewerRef.current.cesiumElement; + const { camera } = viewer; + const minAltitude = + altitude ?? Math.min(DEFAULT_CAMERA_HEIGHT, camera.positionCartographic.height); + const position = Cartesian3.fromDegrees(long, lat, minAltitude); + + setCameraPosition({ + destination: position, + orientation: { + heading: camera.heading, + pitch: pitch ?? camera.pitch, + }, + }); + }; + + useEffect(() => { + registerSetCameraByPosition(setCameraByLongLat); + return () => registerSetCameraByPosition(null); + }, [cameraPosition]); + + useEffect(() => { + const interval = setInterval(() => { + if (viewerRef.current?.cesiumElement) { + setViewerReady(true); + clearInterval(interval); + } + }, 100); + + return () => clearInterval(interval); + }, []); + + useEffect(() => { + if (!viewerReady) return; + const viewer = viewerRef.current.cesiumElement; + const { longitude, latitude, name } = envJson.Origin; + + flyPending.current = true + if (!name || longitude === 0 || latitude === 0) { + flyPending.current = false + } + + // If the user is currently on the sUAS screen, Set the camera + // at the origin coordinates with -90 degrees pitch + if (activeConfigStep === 1) { + const pitch = -Math.PI / 2; + setCameraByLongLat( + envJson.Origin.longitude, + envJson.Origin.latitude, + DEFAULT_CAMERA_HEIGHT, + pitch, + ); + + // Disable camera tilt to lock the pitch angle + viewer.scene.screenSpaceCameraController.enableTilt = false; + } else { + // Enable camera tilt to allow user control over pitch + viewer.scene.screenSpaceCameraController.enableTilt = true; + } + }, [activeConfigStep]); + + // Move camera to the origin whenever the origin's changed + useEffect(() => { + + const { longitude, latitude, name } = envJson.Origin; + flyPending.current = true + if (!name || longitude === 0 || latitude === 0) { + return; + } + + setCameraByLongLat( + envJson.Origin.longitude, + envJson.Origin.latitude, + DEFAULT_CAMERA_HEIGHT, + -Math.PI / 2, + ); + }, [envJson.Origin.latitude, envJson.Origin.longitude, envJson.Origin.height, viewerReady]); + + useEffect(() => { + if (envJson.Origin.name === originTypes.SpecifyRegion) { + findHeight() + .then((h) => { + // Handle the case where findHeight returns null because mainJson initializes + // before the Cesium viewer when reusing the config + if (h == null) { + return; + } + envJson.setOriginHeight(h); + setEnvJson(EnvironmentModel.getReactStateBasedUpdate(envJson)); + }) + .catch((error) => { + console.error('Error fetching height:', error); + }); + } + }, [envJson.Origin.name, viewerReady]); + + useEffect(() => { + const { destination, orientation } = cameraPosition; + const carto = Cartographic.fromCartesian(destination); + console.log('CameraFlyTo triggered:'); + console.log(`Longitude: ${CesiumMath.toDegrees(carto.longitude)}`); + console.log(`Latitude: ${CesiumMath.toDegrees(carto.latitude)}`); + console.log(`Height: ${carto.height}`); + console.log(`Heading (deg): ${CesiumMath.toDegrees(orientation.heading)}`); + console.log(`Pitch (deg): ${CesiumMath.toDegrees(orientation.pitch)}`); + }, [cameraPosition]); + + const findHeight = async () => { + const viewer = viewerRef.current?.cesiumElement; + if (!viewer || !viewer.terrainProvider) { + console.log('Viewer or terrain provider is not initialized'); + return null; + } + const position = Cartographic.fromDegrees(envJson.Origin.longitude, envJson.Origin.latitude); + + // Sample the terrain at the most detailed level available + try { + const positions = [position]; + await sampleTerrainMostDetailed(viewer.terrainProvider, positions); + const height = positions[0].height; + return height; + } catch (error) { + console.error('Failed to get terrain height:', error); + return null; + } + }; + + const terrainProvider = createWorldTerrainAsync(); + + return ( + + + {flyPending.current && ( + { + flyPending.current = false + }} + /> + )} + + + + {/* */} + + + + + + ); +}; + +CesiumMap.propTypes = { + activeConfigStep: PropTypes.number.isRequired, +}; + +export default CesiumMap; \ No newline at end of file diff --git a/frontend/src/components/cesium/DrawSadeZone.jsx b/frontend/src/components/cesium/DrawSadeZone.jsx new file mode 100644 index 000000000..f7a8173c5 --- /dev/null +++ b/frontend/src/components/cesium/DrawSadeZone.jsx @@ -0,0 +1,255 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { Entity } from 'resium'; +import { + ScreenSpaceEventType, + Cartographic, + Color, + Rectangle, + KeyboardEventModifier, + ScreenSpaceEventHandler, + Ellipsoid, + Math as CesiumMath, + Cartesian3, + Cartesian2, + HeightReference, + VerticalOrigin, + DistanceDisplayCondition, +} from 'cesium'; +import PropTypes from 'prop-types'; +import { useMainJson } from '../../contexts/MainJsonContext'; +import { EnvironmentModel } from '../../model/EnvironmentModel'; +import { findRectangleLength, findRectangleWidth } from '../../utils/mapUtils'; +import { imageUrls } from '../../utils/const'; + +const DrawSadeZone = ({ viewerReady, viewerRef, setCameraByPosition }) => { + const { envJson, setEnvJson } = useMainJson(); + const [mouseDown, setMouseDown] = useState(false); + const [firstPoint, setFirstPoint] = useState(null); + const [lastPoint, setLastPoint] = useState(null); + const [zoneHeight, setZoneHeight] = useState(0); + + useEffect(() => { + if (!viewerReady) return; + const viewer = viewerRef.current.cesiumElement; + const handler = new ScreenSpaceEventHandler(viewer.scene.canvas); + + handler.setInputAction( + (movement) => { + if (envJson.activeSadeZoneIndex == null) return; + + setCameraByPosition(); + setMouseDown(true); + viewer.canvas.style.border = '2px dashed red'; + // Disable camera rotation while the user is drawing a Sade-zone + viewer.scene.screenSpaceCameraController.enableRotate = false; + + const cartesian = viewer.scene.pickPosition(movement.position); + if (cartesian) { + const ray = viewer.camera.getPickRay(movement.position); + const intersection = viewer.scene.pickFromRay(ray, []); + let height = 100; + + if (intersection && intersection.position) { + height = Cartographic.fromCartesian(intersection.position).height; + } + setZoneHeight(height); + const cartographic = Cartographic.fromCartesian(cartesian); + // Set the starting point of the rectangle + setFirstPoint(cartographic); + } + }, + ScreenSpaceEventType.LEFT_DOWN, + KeyboardEventModifier.SHIFT, + ); + + handler.setInputAction( + (movement) => { + if (!mouseDown) return; + const cartesian = viewer.scene.pickPosition(movement.endPosition); + if (cartesian && firstPoint) { + const tempCartographic = Cartographic.fromCartesian(cartesian); + setLastPoint(tempCartographic); + const rect = new Rectangle( + Math.min(tempCartographic.longitude, firstPoint.longitude), + Math.min(tempCartographic.latitude, firstPoint.latitude), + Math.max(tempCartographic.longitude, firstPoint.longitude), + Math.max(tempCartographic.latitude, firstPoint.latitude), + ); + updateSadeZone(rect); + } + }, + ScreenSpaceEventType.MOUSE_MOVE, + KeyboardEventModifier.SHIFT, + ); + }, [viewerReady, firstPoint, envJson.activeSadeZoneIndex]); + + useEffect(() => { + if (!viewerReady) return; + const viewer = viewerRef.current.cesiumElement; + const handler = new ScreenSpaceEventHandler(viewer.scene.canvas); + handler.setInputAction( + () => { + // make sure this event listener executes only when activeSadeZoneIndex is not null + if (envJson.activeSadeZoneIndex == null) return; + + viewer.canvas.style.border = 'none'; + viewer.scene.screenSpaceCameraController.enableRotate = true; + + setMouseDown(false); + // Clear the first-point, indicating that the current Sade-zone drawing is finished + setFirstPoint(null); + setCameraByPosition(); + + const currentInx = envJson.activeSadeZoneIndex; + envJson.activeSadeZoneIndex = null; + let sade = envJson.getSadeBasedOnIndex(currentInx); + sade.updateVertices(); + setEnvJson(EnvironmentModel.getReactStateBasedUpdate(envJson)); + }, + ScreenSpaceEventType.LEFT_UP, + KeyboardEventModifier.SHIFT, + ); + }, [lastPoint, envJson.activeSadeZoneIndex]); + + const updateSadeZone = (rect) => { + const currentInx = envJson.activeSadeZoneIndex; + if (currentInx == null) return; + let sade = envJson.getSadeBasedOnIndex(currentInx); + sade.rectangle = rect; + const len = findRectangleLength(rect); + sade.length = len; + const width = findRectangleWidth(rect); + sade.width = width; + // increase height by 10 meters to ensure sade zone is clearly visible on the map while drawing + sade.height = zoneHeight + 10; + // Calculate the center of the rectangle + const centerLongitude = (rect.east + rect.west) / 2; + const centerLatitude = (rect.north + rect.south) / 2; + sade.centerLat = CesiumMath.toDegrees(centerLatitude); + sade.centerLong = CesiumMath.toDegrees(centerLongitude); + envJson.updateSadeBasedOnIndex(currentInx, sade); + setEnvJson(EnvironmentModel.getReactStateBasedUpdate(envJson)); + }; + + return ( + <> + {envJson.getAllSades().map((sade, index) => ( + + {/** rectangle entity representing the sade zone */} + {sade.rectangle && ( + + + + {/** polyline entity representing the length of a sade zone */} + + + + + + {/** polyline entity representing the width of a sade zone */} + + + + + )} + + {sade.centerLong && sade.centerLat && ( + // billboard entity representing the center of the sade zone + + )} + + ))} + + ); +}; + +DrawSadeZone.propTypes = { + viewerReady: PropTypes.bool.isRequired, + viewerRef: PropTypes.object.isRequired, + setCameraByPosition: PropTypes.func.isRequired, +}; + +export default DrawSadeZone; diff --git a/frontend/src/components/cesium/DroneDragAndDrop.jsx b/frontend/src/components/cesium/DroneDragAndDrop.jsx new file mode 100644 index 000000000..794b28910 --- /dev/null +++ b/frontend/src/components/cesium/DroneDragAndDrop.jsx @@ -0,0 +1,147 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { Entity } from 'resium'; +import { + Cartesian3, + Math as CesiumMath, + Cartographic, + Cartesian2, + JulianDate, + Ellipsoid, + Color, + HeightReference, + DistanceDisplayCondition, + VerticalOrigin, + SceneMode, +} from 'cesium'; +import PropTypes from 'prop-types'; +import { useMainJson } from '../../contexts/MainJsonContext'; +import { SimulationConfigurationModel } from '../../model/SimulationConfigurationModel'; +import dayjs from 'dayjs'; +import { EnvironmentModel } from '../../model/EnvironmentModel'; +import { imageUrls } from '../../utils/const'; + +const DroneDragAndDrop = ({ viewerReady, viewerRef, setCameraByPosition }) => { + const { syncDroneLocation, mainJson, setMainJson, envJson, setEnvJson } = useMainJson(); + const [labelVisible, setLabelVisible] = useState(false); + + const checkCameraHeight = () => { + if (!viewerRef.current) return; + const viewer = viewerRef.current.cesiumElement; + if (viewer.scene.mode === SceneMode.SCENE3D) { + const cameraHeight = viewer.camera.positionCartographic.height; + setLabelVisible(cameraHeight < 3000); + } + }; + + // drone drag and drop event listeners + useEffect(() => { + if (viewerReady) { + const viewer = viewerRef.current.cesiumElement; + const canvas = viewer.canvas; + + // Ensure the canvas is focusable + canvas.setAttribute('tabindex', '0'); + + const dragOverHandler = (event) => { + event.preventDefault(); // Necessary to allow the drop + canvas.style.border = '2px dashed red'; // Visual feedback + }; + + const dropHandler = (event) => { + event.preventDefault(); + canvas.style.border = ''; // Remove visual feedback + + const rect = canvas.getBoundingClientRect(); + // Adjust X and Y coordinate relative to the canvas + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const dragData = JSON.parse(event.dataTransfer.getData('text/plain')); + const droneInx = dragData.index; + const cesiumCanvasPosition = new Cartesian2(x, y); + const cartesian = viewer.scene.pickPosition(cesiumCanvasPosition); + if (cartesian) { + const cartographic = Cartographic.fromCartesian(cartesian); + const latitude = CesiumMath.toDegrees(cartographic.latitude); + const longitude = CesiumMath.toDegrees(cartographic.longitude); + + // Use getPickRay to get the building height + const ray = viewer.camera.getPickRay(cesiumCanvasPosition); + const intersection = viewer.scene.pickFromRay(ray, []); + let buildingHeight = 0; + + if (intersection && intersection.position) { + buildingHeight = Cartographic.fromCartesian(intersection.position).height; + } + + setCameraByPosition(); + + if (dragData.type === 'drone') { + syncDroneLocation(latitude, longitude, buildingHeight, droneInx); + } + } + }; + + canvas.addEventListener('dragover', dragOverHandler); + canvas.addEventListener('drop', dropHandler); + viewer.camera.moveEnd.addEventListener(checkCameraHeight); + + return () => { + canvas.removeEventListener('dragover', dragOverHandler); + canvas.removeEventListener('drop', dropHandler); + }; + } + }, [viewerReady, mainJson, envJson]); + + return ( + <> + {mainJson.getAllDrones().map((drone, index) => { + if (!drone.X || !drone.Y || !drone.Z) return null; + const position = Cartesian3.fromDegrees(drone.Y, drone.X, drone.Z); + return ( + + + + + ); + })} + + ); +}; + +DroneDragAndDrop.propTypes = { + viewerReady: PropTypes.bool.isRequired, + viewerRef: PropTypes.object.isRequired, + setCameraByPosition: PropTypes.func.isRequired, +}; + +export default DroneDragAndDrop; diff --git a/frontend/src/components/cesium/RegionDragAndDrop.jsx b/frontend/src/components/cesium/RegionDragAndDrop.jsx new file mode 100644 index 000000000..5294b76be --- /dev/null +++ b/frontend/src/components/cesium/RegionDragAndDrop.jsx @@ -0,0 +1,145 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { Entity } from 'resium'; +import { + Cartesian3, + Math as CesiumMath, + Cartographic, + VerticalOrigin, + Cartesian2, + HeightReference, + JulianDate, + Ellipsoid, + LabelStyle, + Color, + DistanceDisplayCondition, +} from 'cesium'; +import PropTypes from 'prop-types'; +import { useMainJson } from '../../contexts/MainJsonContext'; +import { SimulationConfigurationModel } from '../../model/SimulationConfigurationModel'; + +const RegionDragAndDrop = ({ viewerReady, viewerRef, setCameraByPosition }) => { + const { syncRegionLocation, envJson } = useMainJson(); + + // radius drag and drop event listeners + useEffect(() => { + if (viewerReady) { + const viewer = viewerRef.current.cesiumElement; + const canvas = viewer.canvas; + + viewer.animation.viewModel.timeFormatter = function (date, viewModel) { + date = JulianDate.toDate(date); + let hours = date.getHours(); + let minutes = date.getMinutes(); + let seconds = date.getSeconds(); + if (hours < 10) { + hours = `0${hours}`; + } + if (minutes < 10) { + minutes = `0${minutes}`; + } + if (seconds < 10) { + seconds = `0${seconds}`; + } + return hours + ':' + minutes + ':' + seconds; + }; + + // Ensure the canvas is focusable + canvas.setAttribute('tabindex', '0'); + + const dragOverHandler = (event) => { + event.preventDefault(); // Necessary to allow the drop + canvas.style.border = '2px dashed red'; // Visual feedback + }; + + const dropHandler = (event) => { + event.preventDefault(); + canvas.style.border = ''; // Remove visual feedback + + const rect = canvas.getBoundingClientRect(); + // Adjust X and Y coordinate relative to the canvas + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + const dragData = JSON.parse(event.dataTransfer.getData('text/plain')); + const cesiumCanvasPosition = new Cartesian2(x, y); + const cartesian = viewer.scene.pickPosition(cesiumCanvasPosition); + if (cartesian) { + const cartographic = Cartographic.fromCartesian(cartesian); + const latitude = CesiumMath.toDegrees(cartographic.latitude); + const longitude = CesiumMath.toDegrees(cartographic.longitude); + + // Use getPickRay to get the building height + const ray = viewer.camera.getPickRay(cesiumCanvasPosition); + const intersection = viewer.scene.pickFromRay(ray, []); + let buildingHeight = 0; + + if (intersection && intersection.position) { + buildingHeight = Cartographic.fromCartesian(intersection.position).height; + } + + setCameraByPosition(); + + if (dragData.type == 'region') { + syncRegionLocation(latitude, longitude, buildingHeight, dragData.src); + } + } + }; + + canvas.addEventListener('dragover', dragOverHandler); + canvas.addEventListener('drop', dropHandler); + + return () => { + canvas.removeEventListener('dragover', dragOverHandler); + canvas.removeEventListener('drop', dropHandler); + }; + } + }, [viewerReady, envJson]); + + return ( + + ); +}; + +RegionDragAndDrop.propTypes = { + viewerReady: PropTypes.bool.isRequired, + viewerRef: PropTypes.object.isRequired, + setCameraByPosition: PropTypes.func.isRequired, +}; + +export default RegionDragAndDrop; diff --git a/frontend/src/components/cesium/TimeLineSetterCesiumComponent.jsx b/frontend/src/components/cesium/TimeLineSetterCesiumComponent.jsx new file mode 100644 index 000000000..d6ffaa448 --- /dev/null +++ b/frontend/src/components/cesium/TimeLineSetterCesiumComponent.jsx @@ -0,0 +1,58 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { Entity } from 'resium'; +import { Cartesian3, Math as CesiumMath, Cartographic, Cartesian2, JulianDate } from 'cesium'; +import PropTypes from 'prop-types'; +import { useMainJson } from '../../contexts/MainJsonContext'; +import { SimulationConfigurationModel } from '../../model/SimulationConfigurationModel'; +import dayjs from 'dayjs'; +import { EnvironmentModel } from '../../model/EnvironmentModel'; + +const TimeLineSetterCesiumComponent = ({ viewerReady, viewerRef }) => { + const { mainJson, envJson, setEnvJson, viewerMaintainer, timeOfDayRef, timeRef } = useMainJson(); + + useEffect(() => { + if (viewerReady) { + const viewer = viewerRef.current.cesiumElement; + var clock = viewer.clock; + viewer.clock.onTick.addEventListener((clock) => { + let date; + if (viewerMaintainer.current) { + if (timeRef.current) { + date = JulianDate.fromDate(new Date(timeRef.current)); + } else { + date = JulianDate.fromDate(new Date()); + } + viewer.clock.currentTime = date; + viewerMaintainer.current = false; + } else { + // set the current time to state + date = viewer.clock.currentTime; + const jsDate = JulianDate.toDate(date); + let hours = jsDate.getHours(); + let minutes = jsDate.getMinutes(); + let seconds = jsDate.getSeconds(); + timeRef.current = dayjs(new Date(date)); + if (hours < 10) { + hours = `0${hours}`; + } + if (minutes < 10) { + minutes = `0${minutes}`; + } + if (seconds < 10) { + seconds = `0${seconds}`; + } + timeOfDayRef.current = `${hours}:${minutes}:${seconds}`; + } + }); + } + }, [viewerReady]); + + return <>; +}; + +TimeLineSetterCesiumComponent.propTypes = { + viewerReady: PropTypes.bool.isRequired, + viewerRef: PropTypes.object.isRequired, +}; + +export default TimeLineSetterCesiumComponent; diff --git a/frontend/src/const.js b/frontend/src/const.js deleted file mode 100644 index 443f2e7c5..000000000 --- a/frontend/src/const.js +++ /dev/null @@ -1,4 +0,0 @@ -export const HOME_LABEL = { - partfirst: `Simulation of a realistic scenario is an effective way to test the system's requirements.`, - partsecond: `Describe one or more requirements you would like to test by simulating a scenario`, - }; diff --git a/frontend/src/constants/drone.js b/frontend/src/constants/drone.js new file mode 100644 index 000000000..d0c0ca0f5 --- /dev/null +++ b/frontend/src/constants/drone.js @@ -0,0 +1,46 @@ +export const flightPaths = [ + { value: 'fly_in_circle', label: 'Circle', id: 1 }, + { value: 'fly_to_points', label: 'Square', id: 1 }, + // {value:'fly_straight',label:'Straight', id:1} +]; + +export const droneTypes = [ + { value: 'MultiRotor', label: 'Multi Rotor' }, + // { value: 'FixedWing', label: 'Fixed Wing' }, +]; + +export const droneModels = { + FixedWing: [ + { value: 'SenseflyeBeeX', label: 'Sensefly eBee X', src: '/images/SenseflyeBeeX.png' }, + { value: 'TrinityF90', label: 'Trinity F90', src: '/images/TrinityF90.png' }, + ], + MultiRotor: [ + { value: 'ParrotANAFI', label: 'Parrot ANAFI', src: '/images/Parrot-ANAFI.png' }, + { value: 'DJI', label: 'DJI', src: '/images/DJI.png' }, + { value: 'VOXLm500', label: 'VOXL m500', src: '/images/VOXLm500.png' }, + { value: 'AureliaX6Pro', label: 'Aurelia X6 Pro', src: '/images/Aurelia-X6-Pro.png' }, + { value: 'IF1200', label: 'IF 1200', src: '/images/IF1200.png' }, + { value: 'Crazyflie 2.0', label: 'Crazyflie 2.0', src: '/images/Craziefly2.1.png' }, + { + /*value: 'StreamLineDesignX189', label: 'StreamLineDesign X189', src: null*/ + }, + ], +}; + +export const locations = [ + { value: 'GeoLocation', id: 1 }, + { value: 'Cartesian Coordinate', id: 2 }, +]; + +export const droneImages = [ + { src: '/images/drone-red.png', color: '#FFCCCC' }, + { src: '/images/drone-green.png', color: '#CCFFCC' }, + { src: '/images/drone-blue.png', color: '#CCCCFF' }, + { src: '/images/drone-yellow.png', color: '#FFFFCC' }, + { src: '/images/drone-pink.png', color: '#FFCCFF' }, + { src: '/images/drone-indigo.png', color: '#CCFFFF' }, + { src: '/images/drone-gold.png', color: '#F0E68C' }, + { src: '/images/drone-darkblue.png', color: '#E6E6FA' }, + { src: '/images/drone-orange.png', color: '#FFDAB9' }, + { src: '/images/drone-purple.png', color: '#DABDF9' }, +]; diff --git a/frontend/src/constants/env.js b/frontend/src/constants/env.js new file mode 100644 index 000000000..066848436 --- /dev/null +++ b/frontend/src/constants/env.js @@ -0,0 +1,37 @@ +export const WindDirection = [ + { value: 'N', id: 5 }, + { value: 'S', id: 6 }, + { value: 'E', id: 7 }, + { value: 'W', id: 8 }, + { value: 'NE', id: 1 }, + { value: 'SE', id: 2 }, + { value: 'SW', id: 3 }, + { value: 'NW', id: 4 }, +]; + +export const WindType = [ + { value: 'Constant Wind', id: 1 }, + { value: 'Turbulent Wind', id: 2 }, +]; + +export const ENVIRONMENT_ORIGINS = [ + // { value: 'Michigan Lake Beach', id: 10 }, + { value: 'Chicago O’Hare Airport', id: 20 }, + { value: 'Specify Region', id: 30 }, +]; + +export const originTypes = { + MichiganLakeBeach: 'Michigan Lake Beach', + ChicagoOhareAirport: 'Chicago O’Hare Airport', + SpecifyRegion: 'Specify Region', +}; + +export const ENVIRONMENT_ORIGIN_VALUES = [ + { value: 'Specify Region', latitude: 0, longitude: 0, height: 0 }, + // { value: 'Michigan Lake Beach', latitude: 42.211223, longitude: -86.390394, height: 170 }, + { value: 'Chicago O’Hare Airport', latitude: 41.980381, longitude: -87.934524, height: 200 }, +]; + +export const origin = { + DEFAULT_RADIUS: 0.3, +}; diff --git a/frontend/src/constants/map.js b/frontend/src/constants/map.js new file mode 100644 index 000000000..df402647b --- /dev/null +++ b/frontend/src/constants/map.js @@ -0,0 +1,44 @@ +import { tabEnums } from './simConfig'; +import { imageUrls } from '../utils/const'; + +export const mapControls = { + [tabEnums.ENV_REGION]: { + header: 'ORIGIN DRAG AND DROP', + body: [ + { + icon: [imageUrls.location_orange], + command: 'DRAG ICON', + info: "FROM THE 'SIMULATION ORIGIN' PANEL TO PLACE ON MAP", + }, + ], + }, + [tabEnums.ENV_SADEZONE]: { + header: 'DRAW SADE ZONE', + body: [ + { icon: [imageUrls.sign_up], command: 'CLICK ICON', info: 'TO ACTIVATE SADE ZONE' }, + { + icon: [imageUrls.shift, imageUrls.left_click], + command: 'SHIFT + LEFT-CLICK + DRAG', + info: 'TO DRAW A SADE ZONE', + }, + ], + }, + [tabEnums.DRONES]: { + header: 'DRONE DRAG AND DROP', + body: [ + { + icon: [imageUrls.drone_orange], + command: 'DRAG ICON', + info: 'FROM THE DRONE PANEL TO PLACE ON MAP', + }, + ], + }, + default: { + header: 'MOVEMENT AND CAMERA', + body: [ + { icon: [imageUrls.left_click], command: 'LEFT CLICK + DRAG', info: 'TO PAN' }, + { icon: [imageUrls.middle_click], command: 'MIDDLE CLICK + DRAG', info: 'TO ROTATE' }, + { icon: [imageUrls.mouse_scroll], command: 'SCROLL', info: 'TO ZOOM IN AND OUT' }, + ], + }, +}; diff --git a/frontend/src/constants/simConfig.js b/frontend/src/constants/simConfig.js new file mode 100644 index 000000000..195bff490 --- /dev/null +++ b/frontend/src/constants/simConfig.js @@ -0,0 +1,18 @@ +export const tabEnums = { + ENV_REGION: 'env-region', + ENV_WIND: 'env-wind', + ENV_SADEZONE: 'env-sadezone', + DRONES: 'drones', + }; + + export const configurationTabNames = [ + [tabEnums.ENV_REGION, tabEnums.ENV_WIND, tabEnums.ENV_SADEZONE], + tabEnums.DRONES, + ]; + + export const steps = [ + 'Environment Configuration', + 'sUAS Configuration', + //'Test Configuration' + ]; + \ No newline at end of file diff --git a/frontend/src/contexts/MainJsonContext.js b/frontend/src/contexts/MainJsonContext.js new file mode 100644 index 000000000..dc266ca18 --- /dev/null +++ b/frontend/src/contexts/MainJsonContext.js @@ -0,0 +1,82 @@ +import React, { createContext, useState, useContext, useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { SimulationConfigurationModel } from '../model/SimulationConfigurationModel'; +import { EnvironmentModel } from '../model/EnvironmentModel'; + +const MainJsonContext = createContext(); + +export const useMainJson = () => useContext(MainJsonContext); + +export const MainJsonProvider = ({ children }) => { + const [mainJson, setMainJsonSetter] = useState(new SimulationConfigurationModel()); + const [envJson, setEnvJsonSetter] = useState(mainJson.environment); + const viewerMaintainer = useRef(true); + const timeOfDayRef = useRef(mainJson.TimeOfDay); + const timeRef = useRef(mainJson.time); + // Provide ref to access the camera control function + const setCameraPositionRef = useRef(null); + const [activeScreen, setActiveScreen] = useState(''); + + const setMainJson = (input) => { + envJson.time = timeRef.current; + envJson.TimeOfDay = timeOfDayRef.current; + // input.environment = envJson; + setMainJsonSetter(SimulationConfigurationModel.getReactStateBasedUpdate(input)); + }; + + const setEnvJson = (input) => { + input.time = timeRef.current; + input.TimeOfDay = timeOfDayRef.current; + mainJson.environment = input; + setEnvJsonSetter(EnvironmentModel.getReactStateBasedUpdate(input)); + setMainJsonSetter(SimulationConfigurationModel.getReactStateBasedUpdate(mainJson)); + }; + + function syncDroneLocation(latitude, longitude, height, droneIndex) { + let drone = mainJson.getDroneBasedOnIndex(droneIndex); + drone.X = latitude; + drone.Y = longitude; + drone.Z = height; + mainJson.updateDroneBasedOnIndex(droneIndex, drone); + setMainJson(SimulationConfigurationModel.getReactStateBasedUpdate(mainJson)); + } + + function syncRegionLocation(latitude, longitude, height, image) { + envJson.setOriginLatitude(latitude); + envJson.setOriginLongitude(longitude); + envJson.setOriginHeight(height); + envJson.setOriginImage(image); + setEnvJson(EnvironmentModel.getReactStateBasedUpdate(envJson)); + } + + // function to register the camera control function + const registerSetCameraByPosition = (func) => { + setCameraPositionRef.current = func; + }; + + return ( + + {children} + + ); +}; + +MainJsonProvider.propTypes = { + children: PropTypes.node.isRequired, +}; diff --git a/frontend/src/css/SimulationPageStyles.js b/frontend/src/css/SimulationPageStyles.js new file mode 100644 index 000000000..4796ab764 --- /dev/null +++ b/frontend/src/css/SimulationPageStyles.js @@ -0,0 +1,83 @@ +import styled from '@emotion/styled'; +import Button from '@mui/material/Button'; +import Tab from '@mui/material/Tab'; +import Select from '@mui/material/Select'; +import Tabs from '@mui/material/Tabs'; +import Accordion from '@mui/material/Accordion'; +import InputLabel from '@mui/material/InputLabel'; + +export const simulationMainBoxstyle = { + backgroundImage: 'linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 0.7), rgba(128, 128, 128, 0.5)), url("/images/google-earth-3D.png")', + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundAttachment: 'fixed', /* Keeps the background fixed during scrolling */ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100vw', + height: '100vh', +}; + + +export const StyledButton = styled(Button)` + font-size: 18px; + font-weight: bolder; + color: white; + background-color: #8B4513; + width: 200px; + &:hover { + background-color: #A0522D; + } +`; + +export const StyledSelect = styled(Select)(({ theme }) => ({ + backgroundColor: '#F5F5DC', + '& .MuiInputBase-input': { + padding: '6px 8px', + height: '1em', + } +})); + +export const StyledTab = styled(Tab)({ + textTransform: 'none', + fontSize: 16, + fontWeight: 'bold', + marginRight: 8, + // to-do: use global variables for tab colors + color: '#8B4513', // Shade of brown + backgroundColor: '#F5F5DC', // Beige background + transition: 'background-color 0.3s, color 0.3s', + '&:hover': { + backgroundColor: '#DEB887', // Light brown when hovered + color: '#FFFFFF', + }, + '&.Mui-selected': { + backgroundColor: '#A0522D', // Darker brown when selected + color: '#FFFFFF', + borderBottom: '5px solid #FFB500', + }, +}); + +export const StyledTabs = styled(Tabs)({ + minHeight: '48px', + '.MuiTabs-indicator': { + display: 'none', + }, +}); + + +export const AccordionStyled = styled(Accordion)({ + width: '100%', + padding: '5px', + backgroundColor: 'transparent !important' +}) + + +export const StyledInputLabel = styled(InputLabel)(({ theme }) => ({ + marginRight: 2, + marginLeft: 20, + flexShrink: 0, + color: '#F5F5DC', + width: '200px', + fontSize: '1.2rem', fontFamily: 'Roboto, sans-serif', +})); \ No newline at end of file diff --git a/frontend/src/css/styles.css b/frontend/src/css/styles.css new file mode 100644 index 000000000..9b0823836 --- /dev/null +++ b/frontend/src/css/styles.css @@ -0,0 +1,84 @@ +*{ + box-sizing: border-box; +} + +body{ + margin: 0; + font-family: 'Arial'; +} + +.nav{ + + background-color: #1c34d4; + color: rgb(245, 245, 245); + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; + padding: 0 1rem; +} + +.site-title{ + font-size: 4rem; +} + +.nav ul { + padding: 0; + margin: 0; + list-style: none; + display: flex; + gap: 1rem; +} + +.nav a{ + color: inherit; + text-decoration: none; +} +.nav li.active{ + background-color: #e7e7e7 +} + +.nav li:hover{ + background-color: #2929d4 +} + +.main-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +.button-container { + margin-top: 20px; +} + +html { + font-size: 16px; + } + + @media (max-width: 1200px) { + html { + font-size: 15px; + } + } + + @media (max-width: 992px) { + html { + font-size: 14px; + } + } + + @media (max-width: 768px) { + html { + font-size: 13px; + } + } + + @media (max-width: 576px) { + html { + font-size: 12px; + } + } + \ No newline at end of file diff --git a/frontend/src/index.js b/frontend/src/index.js index c760e5978..47c02a5bb 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,6 +1,6 @@ import React from 'react'; -import ReactDOM from 'react-dom/client'; +import ReactDOM from 'react-dom'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); -root.render(); \ No newline at end of file +root.render(); diff --git a/frontend/src/model/DroneModel.js b/frontend/src/model/DroneModel.js new file mode 100644 index 000000000..134097081 --- /dev/null +++ b/frontend/src/model/DroneModel.js @@ -0,0 +1,225 @@ +import { v4 as uuidv4 } from 'uuid'; + +export class DroneModel { + constructor() { + this._Name = `Drone-${uuidv4().substring(0, 4)}`; + this._FlightController = ''; + this._droneType = ''; + this._droneModel = ''; + this._VehicleType = ''; + this._DefaultVehicleState = ''; + this._EnableCollisionPassthrogh = false; + this._EnableCollisions = false; + this._AllowAPIAlways = false; + this._EnableTrace = false; + this._image = ''; + this._color = ''; + this._X = 0; + this._Y = 0; + this._Z = 0; + this._Pitch = 0; + this._Roll = 0; + this._Yaw = 0; + this._Sensors = ''; + this._MissionValue = ''; + this._Mission = { + name: '', + param: [], + }; + } + + get flightController() { + return this._FlightController; + } + + get droneType() { + return this._droneType; + } + + get droneModel() { + return this._droneModel; + } + + get vehicleType() { + return this._VehicleType; + } + + get defaultVehicleState() { + return this._DefaultVehicleState; + } + + get enableCollisionPassthrogh() { + return this._EnableCollisionPassthrogh; + } + + get enableCollisions() { + return this._EnableCollisions; + } + + get allowAPIAlways() { + return this._AllowAPIAlways; + } + + get enableTrace() { + return this._EnableTrace; + } + + get name() { + return this._Name; + } + + get image() { + return this._image; + } + + get color() { + return this._color; + } + + get X() { + return this._X; + } + + get Y() { + return this._Y; + } + + get Z() { + return this._Z; + } + + get Pitch() { + return this._Pitch; + } + + get Roll() { + return this._Roll; + } + + get Yaw() { + return this._Yaw; + } + + get Sensors() { + return this._Sensors; + } + + get MissionValue() { + return this._MissionValue; + } + + set flightController(value) { + this._FlightController = value; + } + + set droneType(value) { + this._droneType = value; + } + + set droneModel(value) { + this._droneModel = value; + } + + set vehicleType(value) { + this._VehicleType = value; + } + + set defaultVehicleState(value) { + this._DefaultVehicleState = value; + } + + set enableCollisionPassthrogh(value) { + this._EnableCollisionPassthrogh = value; + } + + set enableCollisions(value) { + this._EnableCollisions = value; + } + + set allowAPIAlways(value) { + this._AllowAPIAlways = value; + } + + set enableTrace(value) { + this._EnableTrace = value; + } + + set name(value) { + this._Name = value; + } + + set image(value) { + this._image = value; + } + + set color(value) { + this._color = value; + } + + set X(value) { + this._X = value; + } + + set Y(value) { + this._Y = value; + } + + set Z(value) { + this._Z = value; + } + + set Pitch(value) { + this._Pitch = value; + } + + set Roll(value) { + this._Roll = value; + } + + set Yaw(value) { + this._Yaw = value; + } + + set Sensors(value) { + this._Sensors = value; + } + + set MissionValue(value) { + this._MissionValue = value; + } + + setMissionObjectName(value) { + this._Mission.name = value; + } + + setMissionObjectParams(value) { + this._Mission.param = value; + } + + toJSONString() { + return { + drone_name: this._Name, + flight_controller: this._FlightController, + drone_type: this._droneType, + drone_model: this._droneModel, + vehicle_type: this._VehicleType, + default_vehicle_state: this._DefaultVehicleState, + enable_collision_passthrough: this._EnableCollisionPassthrogh, + enable_collisions: this._EnableCollisions, + allow_api_always: this._AllowAPIAlways, + enable_trace: this._EnableTrace, + name: this._Name, + // image: this._image, + color: this._color, + x: this._X, + y: this._Y, + z: this._Z, + pitch: this._Pitch, + roll: this._Roll, + yaw: this._Yaw, + sensors: this._Sensors, + // mission_value: this._MissionValue, + // mission: this._Mission, + }; + } +} diff --git a/frontend/src/model/EnvironmentModel.js b/frontend/src/model/EnvironmentModel.js new file mode 100644 index 000000000..5cb6a4079 --- /dev/null +++ b/frontend/src/model/EnvironmentModel.js @@ -0,0 +1,266 @@ +import { imageUrls } from '../utils/const'; + +import { origin } from '../constants/env'; + +export class EnvironmentModel { + constructor() { + this._name = ''; + this._description = ''; + this._enableFuzzy = false; + this._timeOfDayFuzzy = false; + this._positionFuzzy = false; + this._windFuzzy = false; + this._TimeOfDay = null; + this._UseGeo = false; + this._time = null; + this._Wind = []; + this._Origin = { + latitude: 0, + longitude: 0, + name: '', + height: 0, + radius: origin.DEFAULT_RADIUS, + image: imageUrls.location, + }; + this._sades = []; + this._activeSadeZoneIndex = null; + } + + // Getters + get enableFuzzy() { + return this._enableFuzzy; + } + + get timeOfDayFuzzy() { + return this._timeOfDayFuzzy; + } + + get positionFuzzy() { + return this._positionFuzzy; + } + + get windFuzzy() { + return this._windFuzzy; + } + + get TimeOfDay() { + return this._TimeOfDay; + } + + get UseGeo() { + return this._UseGeo; + } + + get time() { + return this._time; + } + + get Wind() { + return this._Wind; + } + + get Origin() { + return this._Origin; + } + + get activeSadeZoneIndex() { + return this._activeSadeZoneIndex; + } + + get name() { + return this._name; + } + + get description() { + return this._description; + } + + // Setters + set enableFuzzy(value) { + this._enableFuzzy = value; + } + + set timeOfDayFuzzy(value) { + this._timeOfDayFuzzy = value; + } + + set positionFuzzy(value) { + this._positionFuzzy = value; + } + + set windFuzzy(value) { + this._windFuzzy = value; + } + + set TimeOfDay(value) { + this._TimeOfDay = value; + } + + set UseGeo(value) { + this._UseGeo = value; + } + + set time(value) { + this._time = value; + } + + set Origin(value) { + this._Origin = value; + } + + set Wind(value) { + this._Wind = value; + } + + set activeSadeZoneIndex(value) { + this._activeSadeZoneIndex = value; + } + + set name(value) { + this._name = value; + } + + set description(value) { + this._description = value; + } + + getOriginLatitude() { + return this._Origin.latitude; + } + + getOriginLongitude() { + return this._Origin.longitude; + } + + getOriginRadius() { + return this._Origin.radius; + } + + getOriginHeight() { + return this._Origin.height; + } + + getOriginName() { + return this._Origin.name; + } + + getOriginImage() { + return this._Origin.image; + } + + setOriginLatitude(value) { + this._Origin.latitude = value; + } + + setOriginLongitude(value) { + this._Origin.longitude = value; + } + + setOriginRadius(value) { + this._Origin.radius = value; + } + + setOriginHeight(value) { + this._Origin.height = value; + } + + setOriginName(value) { + this._Origin.name = value; + } + + setOriginImage(value) { + this._Origin.image = value; + } + + addNewWind(windObj) { + this._Wind.push(windObj); + } + + getWindBasedOnIndex(index) { + if (this._Wind.length > index) { + return this._Wind[index]; + } + } + + updateWindBasedOnIndex(index, windModel) { + this._Wind[index] = windModel; + } + + deleteWindBasedOnIndex(index) { + this._Wind = this._Wind.filter((_, i) => i !== index); + } + + getAllSades() { + return this._sades; + } + + getSadesCount() { + return this._sades.length; + } + + getSadeBasedOnIndex(index) { + if (this._sades.length > index) { + return this._sades[index]; + } + } + + addNewSade(sadeObj) { + this._sades.push(sadeObj); + } + + updateSadeBasedOnIndex(index, sade) { + this._sades[index] = sade; + } + + deleteSadeBasedOnIndex(index) { + this._sades = this._sades.filter((_, i) => i !== index); + } + + popLastSade() { + this._sades.pop(); + } + + static getReactStateBasedUpdate(instance) { + let model = new EnvironmentModel(); + model.name = instance.name; + model.description = instance.description; + model.enableFuzzy = instance.enableFuzzy; + model.timeOfDayFuzzy = instance.timeOfDayFuzzy; + model.positionFuzzy = instance.positionFuzzy; + model.setOriginLatitude(instance._Origin.latitude); + model.setOriginLongitude(instance._Origin.longitude); + model.setOriginHeight(instance._Origin.height); + model.setOriginRadius(instance._Origin.radius); + model.setOriginName(instance._Origin.name); + model.TimeOfDay = instance.TimeOfDay; + model.UseGeo = instance.UseGeo; + model.time = instance.time; + model.Origin = instance.Origin; + model.Wind = instance.Wind; + model.activeSadeZoneIndex = instance.activeSadeZoneIndex; + const sades = instance.getAllSades(); + for (let i = 0; i < sades.length; i++) { + model.addNewSade(sades[i]); + } + return model; + } + + toJSONString() { + let origin = this._Origin; + delete origin.image; + return { + name: this._name, + description: this._description, + // "enable_fuzzy": this._enableFuzzy, + // "time_of_day_fuzzy": this._timeOfDayFuzzy, + // "position_fuzzy": this._positionFuzzy, + // "wind_fuzzy": this._windFuzzy, + wind: this._Wind?.map((obj) => obj.toJSONString()), + origin: origin, + time_of_day: this._TimeOfDay, + use_geo: this.UseGeo, + time: this._time, + sades: this._sades?.map((obj) => obj.toJSONString()), + }; + } +} diff --git a/frontend/src/model/MonitorModel.js b/frontend/src/model/MonitorModel.js new file mode 100644 index 000000000..7157fc60d --- /dev/null +++ b/frontend/src/model/MonitorModel.js @@ -0,0 +1,12 @@ + + +export class MonitorModel { + + constructor(){ + } + + toJSONString(){ + return {}; + } + +} \ No newline at end of file diff --git a/frontend/src/model/SimulationConfigurationModel.js b/frontend/src/model/SimulationConfigurationModel.js new file mode 100644 index 000000000..37adb4390 --- /dev/null +++ b/frontend/src/model/SimulationConfigurationModel.js @@ -0,0 +1,83 @@ +import { EnvironmentModel } from './EnvironmentModel'; +import { MonitorModel } from './MonitorModel'; +import dayjs from 'dayjs'; + +export class SimulationConfigurationModel { + constructor() { + const currentDate = new Date(); + this._environment = new EnvironmentModel(); + this._environment.enableFuzzy = false; + this._environment.timeOfDayFuzzy = false; + this._environment.positionFuzzy = false; + this._environment.TimeOfDay = currentDate.toUTCString().substring(17, 25); + this._environment.UseGeo = true; + this._environment.time = dayjs(currentDate); + this._drones = new Array(); + this._monitors = new MonitorModel(); + } + + get environment() { + return this._environment; + } + + get monitors() { + return this._monitors; + } + + set environment(value) { + this._environment = value; + } + + set monitors(value) { + this._monitors = value; + } + + set drones(value) { + this._drones = value; + } + + getAllDrones() { + return this._drones; + } + + getDroneBasedOnIndex(index) { + if (this._drones.length > index) { + return this._drones[index]; + } + } + + addNewDrone(droneObj) { + this._drones.push(droneObj); + } + + updateDroneBasedOnIndex(index, drone) { + this._drones[index] = drone; + } + + deleteDroneBasedOnIndex(index) { + this._drones = this._drones.filter((_, i) => i !== index); + } + + popLastDrone() { + this._drones.pop(); + } + + static getReactStateBasedUpdate(instance) { + let model = new SimulationConfigurationModel(); + model.environment = instance.environment; + model.monitors = instance.monitors; + const drones = instance.getAllDrones(); + for (let i = 0; i < drones.length; i++) { + model.addNewDrone(drones[i]); + } + return model; + } + + toJSONString() { + let data = {}; + data['environment'] = this._environment.toJSONString(); + data['drones'] = this._drones?.map((droneObj) => droneObj.toJSONString()); + // data["monitors"] = this._monitors.toJSONString(); + return data; + } +} diff --git a/frontend/src/pages/AboutUs.jsx b/frontend/src/pages/AboutUs.jsx new file mode 100644 index 000000000..8a64455cd --- /dev/null +++ b/frontend/src/pages/AboutUs.jsx @@ -0,0 +1,294 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Box, Container, Grid, Paper, Typography } from '@mui/material'; +import { makeStyles } from '@mui/styles'; + +const useStyles = makeStyles((theme) => ({ + pageContainer: { + minHeight: '100vh', + backgroundColor: '#1e40af', + padding: '2rem 0', + }, + glassTile: { + background: 'rgba(255, 255, 255, 0.05)', + backdropFilter: 'blur(15px)', + border: '1px solid rgba(255, 255, 255, 0.1)', + borderRadius: '32px', + padding: '4rem', + color: '#fff', + textAlign: 'center', + height: '400px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 0.3s ease', + margin: '0 0.5rem', + '&:hover': { + filter: 'brightness(0.75)', + }, + }, + fullWidthTile: { + width: 'calc(100% - 1rem)', + marginBottom: '2rem', + marginLeft: '0.5rem', + marginRight: '0.5rem', + background: 'rgba(255, 255, 255, 0.05)', + backdropFilter: 'blur(15px)', + border: '1px solid rgba(255, 255, 255, 0.1)', + borderRadius: '32px', + padding: '50px 60px', + color: '#fff', + textAlign: 'center', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 0.3s ease', + '&:hover': { + filter: 'brightness(0.75)', + }, + }, + tableTile: { + width: 'calc(100% - 1rem)', + marginBottom: '2rem', + marginLeft: '0.5rem', + marginRight: '0.5rem', + background: 'rgba(255, 255, 255, 0.05)', + backdropFilter: 'blur(15px)', + border: '1px solid rgba(255, 255, 255, 0.1)', + borderRadius: '32px', + padding: '50px 60px', + color: '#fff', + textAlign: 'center', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 0.3s ease', + '&:hover': { + filter: 'brightness(0.75)', + }, + }, + gradientTile: { + width: 'calc(100% - 1rem)', + marginBottom: '2rem', + marginLeft: '0.5rem', + marginRight: '0.5rem', + background: 'linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #3730a3 100%)', + backdropFilter: 'none', + border: 'none', + borderRadius: '32px', + padding: '6rem', + color: '#fff', + textAlign: 'center', + minHeight: '120px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 0.3s ease', + '&:hover': { + filter: 'brightness(0.75)', + }, + }, + gridContainer: { + marginBottom: '2rem', + }, + title: { + color: '#fff', + fontWeight: 700, + marginBottom: '3rem', + textAlign: 'center', + }, +})); + +const commonStyles = { + emoji: { + fontSize: '3rem', + marginBottom: '1rem', + }, + heading: { + color: '#000', + fontWeight: 'bold', + marginBottom: '1rem', + }, + description: { + fontSize: '1rem', + color: '#374151', + lineHeight: 1.6, + fontWeight: 'normal', + }, + tableHeader: { + fontSize: '1.5rem', + fontWeight: 'bold', + color: '#1e40af', + }, + tableDescription: { + fontSize: '1rem', + color: '#374151', + fontWeight: 'light', + }, +}; + +const GridTile = ({ emoji, heading, description }) => ( + + {emoji} + + {heading} + + + {description} + + +); + +GridTile.propTypes = { + emoji: PropTypes.string.isRequired, + heading: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, +}; + +function AboutUs() { + const classes = useStyles(); + + return ( + + + + + + About Drone World + + + Revolutionizing sUAS testing through innovative simulation ecosystems and automated + testing solutions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Dr. Ankit Agrawal + + OSS-SLU Team + + MIT License + + Since 2023 +
+ Project Client + + Development Team + + Open Source + + Active Development +
+
+
+ + + + + Advanced sUAS Testing Platform + + + DroneWorld, developed by Dr. Ankit Agrawal and the OSS-SLU team, is an advanced + simulation platform for testing small unmanned aerial systems (sUAS). Our platform + enables users to configure detailed test scenarios by specifying environmental + conditions, sUAS capabilities, and mission objective. By generating realistic 3D + simulation environments and monitoring data for safety compliance, we produce + comprehensive test reports that help developers refine their systems and iterate more + rapidly on complex missions. + + + +
+
+ ); +} + +export default AboutUs; diff --git a/frontend/src/pages/Footer.jsx b/frontend/src/pages/Footer.jsx new file mode 100644 index 000000000..86503f4cc --- /dev/null +++ b/frontend/src/pages/Footer.jsx @@ -0,0 +1,69 @@ +import { Link } from 'react-router-dom'; + +const commonButtonStyle = { + textDecoration: 'none', + padding: '8px 16px', + borderRadius: '6px', + backgroundColor: 'white', + color: '#8c8c8c', + fontWeight: 300, + fontSize: '14px', +}; + +const disabledButtonStyle = { + ...commonButtonStyle, + border: 'none', + cursor: 'not-allowed', +}; + +const containerStyle = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + padding: '16px', + fontFamily: 'Arial, sans-serif', + backgroundColor: 'white', + color: '#8c8c8c', + fontWeight: 300, + fontSize: '14px', +}; + +function Footer() { + return ( + <> +
+ + Documentation + + + + GitHub + + + +
+ +
© 2024 DroneWorld. Built by OSS-SLU. All rights reserved.
+ + ); +} + +export default Footer; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 684ca2f3b..03909dec2 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,5 +1,16 @@ import styled from '@emotion/styled'; -import { Box, Button, TextField, Modal, Typography, Grid, InputLabel, MenuItem, FormControl, Select } from '@mui/material'; +import { + Box, + Button, + TextField, + Modal, + Typography, + Grid, + InputLabel, + MenuItem, + FormControl, + Select, +} from '@mui/material'; import { Link } from 'react-router-dom'; import * as React from 'react'; import { useState, useEffect } from 'react'; @@ -36,7 +47,7 @@ const Home = () => { const [title, setTitle] = useState(''); const [backendInfo, setBackendInfo] = useState({ numQueuedTasks: 0, - backendStatus: 'idle' + backendStatus: 'idle', }); const [open, setOpen] = useState(false); // State for modal @@ -56,15 +67,21 @@ const Home = () => { const handleReqIdChange = (event) => { setSelectedValue(event.target.value); - if (event.target.value === "UAV-301") { - setText('Two sUAS (Small Unmanned Aircraft System) shall be able to complete a circular and square flight mission in windy weather conditions without colliding with stationary objects, the terrain, or other aircraft and drifting from its planned path by more than 10 meters.'); - setTitle("Circular and Square Flight Mission in Windy Weather") - } else if (event.target.value === "UAV-302") { - setText('Two sUAS (Small Unmanned Aircraft Systems) shall be able to complete their missions in windy weather conditions while maintaining a minimum separation distance of at least 5 meters between each other and without drifting by more than 5 meters.'); - setTitle("sUAS Mission Coordination in Windy Weather") - } else if (event.target.value === "UAV-303") { - setText('Two sUAS (Small Unmanned Aircraft Systems) shall be able to complete their respective missions in windy weather conditions without drifting from their planned path by more than 15 meters.'); - setTitle("sUAS Mission in Windy Weather with Path Accuracy") + if (event.target.value === 'UAV-301') { + setText( + 'Two sUAS (Small Unmanned Aircraft System) shall be able to complete a circular and square flight mission in windy weather conditions without colliding with stationary objects, the terrain, or other aircraft and drifting from its planned path by more than 10 meters.', + ); + setTitle('Circular and Square Flight Mission in Windy Weather'); + } else if (event.target.value === 'UAV-302') { + setText( + 'Two sUAS (Small Unmanned Aircraft Systems) shall be able to complete their missions in windy weather conditions while maintaining a minimum separation distance of at least 5 meters between each other and without drifting by more than 5 meters.', + ); + setTitle('sUAS Mission Coordination in Windy Weather'); + } else if (event.target.value === 'UAV-303') { + setText( + 'Two sUAS (Small Unmanned Aircraft Systems) shall be able to complete their respective missions in windy weather conditions without drifting from their planned path by more than 15 meters.', + ); + setTitle('sUAS Mission in Windy Weather with Path Accuracy'); } }; @@ -79,9 +96,9 @@ const Home = () => { }) .then((data) => { const [status, queueSize] = data.split(', '); - if (status === "None") { + if (status === 'None') { setBackendInfo({ numQueuedTasks: 0, backendStatus: 'idle' }); - } else if (status === "Running") { + } else if (status === 'Running') { setBackendInfo({ numQueuedTasks: parseInt(queueSize), backendStatus: 'running' }); } }) @@ -110,115 +127,125 @@ const Home = () => { return ( - + - Backend Status: {backendInfo.backendStatus} - - -
-
- - Queued Tasks: {backendInfo.numQueuedTasks} + Backend Status: {backendInfo.backendStatus} + +
+
+ Queued Tasks: {backendInfo.numQueuedTasks}
-
- - - :not(style)': { - m: 1, - width: 1000, - }, - }} - > - - Requirement ID - - - - - {selectedvalue === '' ? text : `Title: ${title}`} - - - - {selectedvalue !== '' && `Description: ${text}`} - - - - - Start Scenario Configuration - - - - - {/*Remove this code when implementing the header About us. Widen the component. Add a close button.*/} - {/* Button to test Modal*/} - {/* */} - - {/* Modal Component */} - setOpen(false)} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" -> - - {/* Close Button */} - - - - {/* Title Content */} - - - -

-About Drone World -

-

-Drone World is revolutionizing sUAS (small Uncrewed Aerial Systems) testing. In the dynamic world -of sUAS, safety and reliability are paramount. Traditional field testing across diverse environments is costly -and challenging. -

-

-Drone World offers an innovative sUAV simulation ecosystem that generates high-fidelity, realistic environments -mimicking real-world complexities like adverse weather and wireless interference. Our automated solution allows -developers to specify constraints and generate tailored test environments. -

-

-The program monitors sUAV activities against predefined safety parameters and generates detailed acceptance test -reports. This approach provides actionable insights for effective debugging and analysis, enhancing the safety, -reliability, and efficiency of sUAS applications. -

-
-
-
- -
- -); + + Requirement ID + + + + + {selectedvalue === '' ? text : `Title: ${title}`} + + + + {selectedvalue !== '' && `Description: ${text}`} + + + + + Start Scenario Configuration + + + + {/*Remove this code when implementing the header About us. Widen the component. Add a close button.*/} + {/* Button to test Modal*/} + {/* */} + + {/* Modal Component */} + setOpen(false)} + aria-labelledby='modal-modal-title' + aria-describedby='modal-modal-description' + > + + {/* Close Button */} + + + + {/* Title Content */} + + + +

+ About Drone World +

+

+ Drone World is revolutionizing sUAS (small Uncrewed Aerial Systems) testing. In the + dynamic world of sUAS, safety and reliability are paramount. Traditional field + testing across diverse environments is costly and challenging. +

+

+ Drone World offers an innovative sUAV simulation ecosystem that generates + high-fidelity, realistic environments mimicking real-world complexities like adverse + weather and wireless interference. Our automated solution allows developers to + specify constraints and generate tailored test environments. +

+

+ The program monitors sUAV activities against predefined safety parameters and + generates detailed acceptance test reports. This approach provides actionable + insights for effective debugging and analysis, enhancing the safety, reliability, + and efficiency of sUAS applications. +

+
+
+
+ + + ); }; export default Home; - diff --git a/frontend/src/pages/NavigationBar.jsx b/frontend/src/pages/NavigationBar.jsx new file mode 100644 index 000000000..bbaef59c7 --- /dev/null +++ b/frontend/src/pages/NavigationBar.jsx @@ -0,0 +1,153 @@ +import React, { useState } from 'react'; +import "../styles.css" +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Modal from '@mui/material/Modal'; +import Typography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; +import HomeIcon from '@mui/icons-material/Home'; +import Tooltip from '@mui/material/Tooltip'; +import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; +import BackendHealthTitle from '../components/BackendHealthTitle' + +const useStyles = makeStyles((theme) => ({ + nav: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '1.5rem', + backgroundColor: '#6ddaed', + fontFamily: 'Arial, sans-serif', + }, + siteTitle: { + color: 'black', + textDecoration: 'none', + fontSize: '1.5rem', + fontWeight: 'bold', + fontFamily: 'Arial, sans-serif', + }, + navList: { + listStyleType: 'none', + margin: 0, + padding: 0, + fontFamily: 'Arial, sans-serif', + }, + navListItem: { + display: 'inline-block', + marginLeft: '1rem', + textDecoration: 'none', + color: 'black', + padding: '0.5rem 1rem', + borderRadius: '30px', + transition: 'background-color 0.3s ease' + }, + aboutLink: { + textDecoration: 'none', + color: '#fff', + padding: '0.5rem 1rem', + borderRadius: '30px', + transition: 'background-color 0.3s ease', + }, + navLink: { + textDecoration : 'none', + color : 'black', + display : 'inline-block', + '&:hover': { + color: 'white', + transform : 'scale(1.2)' + } + + }, + active: { + textDecoration: "underline", + fontWeight: "bold", + }, +})); +const modalStyle = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 800, + height: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +function NavigationBar() { + const classes = useStyles(); + const location = useLocation(); + const [open, setOpen] = useState(false); + + return ( +
+ + + {/* About Us Modal */} + setOpen(false)} + aria-labelledby="modal-modal-title" + aria-describedby="modal-modal-description" + > + + {/* Close Button */} + + + {/* Title Content */} + + +

+ About Drone World +

+

+ Drone World is revolutionizing sUAS (small Uncrewed Aerial Systems) testing. In the dynamic world of sUAS, safety and reliability are paramount. Traditional field testing across diverse environments is costly and challenging. +

+

+ Drone World offers an innovative sUAV simulation ecosystem that generates high-fidelity, realistic environments mimicking real-world complexities like adverse weather and wireless interference. Our automated solution allows developers to specify constraints and generate tailored test environments. +

+

+ The program monitors sUAV activities against predefined safety parameters and generates detailed acceptance test reports. This approach provides actionable insights for effective debugging and analysis, enhancing the safety, reliability, and efficiency of sUAS applications. +

+
+
+
+
+ ); +} + +export default NavigationBar; \ No newline at end of file diff --git a/frontend/src/pages/NotFound.jsx b/frontend/src/pages/NotFound.jsx new file mode 100644 index 000000000..d91d3334f --- /dev/null +++ b/frontend/src/pages/NotFound.jsx @@ -0,0 +1,102 @@ +import React, { useEffect, useState } from 'react'; +import styled from '@emotion/styled'; +import { Button } from '@mui/material'; +import { Link } from 'react-router-dom'; +import NotFoundImage from '../Assets/Images/NotFound.svg'; + +const NotFoundContainer = styled.div` + height: calc(100vh - 5.3em); + width: 100vw; + align-items: center; + justify-content: space-between; + display: block; + + @media (min-width: 768px) { + display: flex; + flex-direction: row-reverse; + } +`; + +const NotFoundImgContainer = styled.div` + width: 100%; + display: flex; + align-items: center; + padding: 5px; + + @media (min-width: 768px) { + width: 50%; + padding: 50px; + height: 100%; + } + + @media (min-width: 600px) and (max-width: 1024px) { + padding: 10px; + } +`; + +const NotFoundMessage = styled.div` + width: 100%; + padding: 0; + + @media (min-width: 768px) { + width: 50%; + padding: 50px; + } + + @media (min-width: 600px) and (max-width: 1024px) { + padding: 10px; + } +`; + +const NotFoundChild = styled.div` + width: 90%; + margin: auto; + + @media (min-width: 768px) { + width: 70%; + } + + @media (min-width: 768px) and (max-width: 1024px) { + width: 80%; + } +`; + +const RemoveMarginPadding = { + margin: '0 0 20px 0', + padding: 0, +}; + +function NotFound() { + return ( + + + + + + +

Something is not right...

+

+ The page you are trying to open does not exist. You may have mistyped the address, or + the page may have been moved to a different URL. If you believe this is an error, please + contact support. +

+ + + +
+
+
+ ); +} + +export default NotFound; diff --git a/frontend/src/pages/SimulationPage.jsx b/frontend/src/pages/SimulationPage.jsx new file mode 100644 index 000000000..11e6bdf8a --- /dev/null +++ b/frontend/src/pages/SimulationPage.jsx @@ -0,0 +1,19 @@ +import { Box } from '@mui/material'; +import { Navigate, useLocation, useSearchParams } from 'react-router-dom'; +import SimulationController from '../components/Configuration/SimulationController'; +import { MainJsonProvider } from '../contexts/MainJsonContext'; +import { simulationMainBoxstyle } from '../css/SimulationPageStyles'; + +const Simulation = () => { + return ( + <> + + + + + + + ); +}; + +export default Simulation; diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js new file mode 100644 index 000000000..331666cea --- /dev/null +++ b/frontend/src/setupTests.js @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; \ No newline at end of file diff --git a/frontend/src/styles.css b/frontend/src/styles.css index eb49609e7..c3ebbd110 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,54 +1,52 @@ -*{ - box-sizing: border-box; -} - -body{ - margin: 0; -} - -.nav{ - - background-color: #1c34d4; - color: rgb(245, 245, 245); - display: flex; - justify-content: space-between; - align-items: center; - gap: 2rem; - padding: 0 1rem; -} +* { + box-sizing: border-box; +} -.site-title{ +body { + margin: 0; +} + +.nav { + background-color: #1c34d4; + color: rgb(245, 245, 245); + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; + padding: 0 1rem; +} + +.site-title { font-size: 4rem; -} +} -.nav ul { - padding: 0; - margin: 0; - list-style: none; - display: flex; +.nav ul { + padding: 0; + margin: 0; + list-style: none; + display: flex; gap: 1rem; -} +} -.nav a{ - color: inherit; - text-decoration: none; -} -.nav li.active{ - background-color: #e7e7e7 -} +.nav a { + color: inherit; + text-decoration: none; +} + +.nav li.active { + background-color: #e7e7e7; +} + +.nav li:hover { + background-color: #2929d4; +} -.nav li:hover{ - background-color: #2929d4 -} .main-content { display: flex; flex-direction: column; align-items: center; justify-content: center; - height: 100vh; -} + height: 100vh; +} -.button-container { - margin-top: 20px; -} \ No newline at end of file diff --git a/frontend/src/tests/App.test.js b/frontend/src/tests/App.test.js new file mode 100644 index 000000000..81e8a759a --- /dev/null +++ b/frontend/src/tests/App.test.js @@ -0,0 +1,16 @@ +/* eslint-env jest */ +import { render, screen } from '@testing-library/react'; +import App from '../App'; + +test('renders DroneWorld header text', () => { + render(); + const header = screen.getByText(/Drone World/i); + expect(header).toBeInTheDocument(); +}); + +test('About Us link exists and routes to /aboutus', () => { + render(); + const aboutUsLink = screen.getByRole('link', { name: /about us/i }); + expect(aboutUsLink).toBeInTheDocument(); + expect(aboutUsLink).toHaveAttribute('href', '/aboutus'); +}); \ No newline at end of file diff --git a/frontend/src/utils/const.js b/frontend/src/utils/const.js new file mode 100644 index 000000000..8b692e5c1 --- /dev/null +++ b/frontend/src/utils/const.js @@ -0,0 +1,42 @@ +export const HOME_LABEL = { + partfirst: `Simulation of a realistic scenario is an effective way to test the system's requirements.`, + partsecond: `Describe one or more requirements you would like to test by simulating a scenario`, +}; + +// export const BASE_URL = 'http://localhost:5000'; +let API_URL = 'http://localhost:8000'; +if(process.env.REACT_APP_API_URL){ + API_URL = process.env.REACT_APP_API_URL; +} +export const BASE_URL = API_URL; + +export const UAV_DESCRIPTION = { + 'UAV-301': { + text: 'Two sUAS (Small Unmanned Aircraft System) shall be able to complete a circular and square flight mission in windy weather conditions without colliding with stationary objects, the terrain, or other aircraft and drifting from its planned path by more than 10 meters.', + title: 'Circular and Square Flight Mission in Windy Weather', + }, + 'UAV-302': { + text: 'Two sUAS (Small Unmanned Aircraft Systems) shall be able to complete their missions in windy weather conditions while maintaining a minimum separation distance of at least 5 meters between each other and without drifting by more than 5 meters.', + title: 'sUAS Mission Coordination in Windy Weather', + }, + 'UAV-303': { + text: 'Two sUAS (Small Unmanned Aircraft Systems) shall be able to complete their respective missions in windy weather conditions without drifting from their planned path by more than 15 meters.', + title: 'sUAS Mission in Windy Weather with Path Accuracy', + }, +}; + +export const imageUrls = { + drone_icon: '/images/drone-icon.png', + pin: '/images/map-pin.png', + left_click: '/images/left-click.png', + middle_click: '/images/middle-click.png', + right_click: '/images/right-click.png', + mouse_scroll: '/images/mouse-scroll.png', + drone_orange: '/images/drone-orange.png', + drone_thick_orange: '/images/drone-thick-orange.png', + sign_up: 'images/sign-up.png', + shift: 'images/shift.png', + auth_drone: 'images/auth-drone.png', + location_orange: 'images/location-orange.png', + location: 'images/location.png', +}; diff --git a/frontend/src/utils/mapUtils.js b/frontend/src/utils/mapUtils.js new file mode 100644 index 000000000..2ef8e10d5 --- /dev/null +++ b/frontend/src/utils/mapUtils.js @@ -0,0 +1,80 @@ +import { Rectangle } from 'cesium'; + +function degreesToRadians(degrees) { + return (degrees * Math.PI) / 180; +} + +export function distanceInMetersBetweenEarthCoords(lat1, lon1, lat2, lon2) { + var earthRadiusKm = 6371; + // lat1, lon1, lat2, lon2 parameters must be in radians + var dLat = lat2 - lat1; + var dLon = lon2 - lon1; + + var a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return earthRadiusKm * c * 1000; +} + +export const findRectangleWidth = (rectangle) => { + return distanceInMetersBetweenEarthCoords( + (rectangle.north + rectangle.south) / 2, + rectangle.west, + (rectangle.north + rectangle.south) / 2, + rectangle.east, + ); +}; + +export const findRectangleLength = (rectangle) => { + return distanceInMetersBetweenEarthCoords( + rectangle.north, + (rectangle.west + rectangle.east) / 2, + rectangle.south, + (rectangle.west + rectangle.east) / 2, + ); +}; + +export function metersToLatitude(meters) { + return meters / 111000; // One degree of latitude is approximately 111 km +} + +export function metersToLongitude(meters, latitude) { + // Convert latitude to radians for the cosine function + const latitudeInRadians = degreesToRadians(latitude); + // Longitude in degrees depends on latitude + return meters / (111000 * Math.cos(latitudeInRadians)); +} + +export function updateRectangle(centerLon, centerLat, length, width) { + const halfWidthInDegrees = metersToLongitude(width / 2, centerLat); + const halfLengthInDegrees = metersToLatitude(length / 2); + + return new Rectangle.fromDegrees( + centerLon - halfWidthInDegrees, + centerLat - halfLengthInDegrees, + centerLon + halfWidthInDegrees, + centerLat + halfLengthInDegrees, + ); +} + +export function findRectangleCorners(centerLon, centerLat, length, width) { + const halfWidthInDegrees = metersToLongitude(width / 2, centerLat); + const halfLengthInDegrees = metersToLatitude(length / 2); + + const lon1 = centerLon - halfWidthInDegrees; + const lon2 = centerLon + halfWidthInDegrees; + const lat1 = centerLat - halfLengthInDegrees; + const lat2 = centerLat + halfLengthInDegrees; + + return { + lat1: lat2, + long1: lon1, // Top left + lat2: lat2, + long2: lon2, // Top right + lat3: lat1, + long3: lon2, // Bottom right + lat4: lat1, + long4: lon1, // Bottom left + }; +} diff --git a/frontend/src/utils/regionUtils.js b/frontend/src/utils/regionUtils.js new file mode 100644 index 000000000..f47d26764 --- /dev/null +++ b/frontend/src/utils/regionUtils.js @@ -0,0 +1,24 @@ +export const findCurrentLocation = async () => { + return new Promise((resolve, reject) => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const coords = { + latitude: position.coords.latitude, + longitude: position.coords.longitude, + height: 0, + }; + resolve(coords); + }, + (error) => { + console.error('Error obtaining location: ', error); + reject(); + }, + ); + } else { + console.error('Geolocation is not supported by this browser.'); + reject(); + } + }); + }; + \ No newline at end of file diff --git a/img.png b/img.png deleted file mode 100644 index 683a4a678..000000000 Binary files a/img.png and /dev/null differ diff --git a/pixelstreaming.md b/pixelstreaming.md new file mode 100644 index 000000000..9671a7815 --- /dev/null +++ b/pixelstreaming.md @@ -0,0 +1,10 @@ +# Steps + +- In the DRV navigate to the below directory +> \DRV_1.0.0_UI_finalize\Windows\Samples\PixelStreaming\WebServers\SignallingWebServer\platform_scripts\cmd +- Click on “setup” and run it +- In the same location click on “run_local” and run it +- Open the DRV directory(one in which Blocks.exe is present) in a shell and run the below command +> ./Blocks.exe -PixelStreamingURL=ws://127.0.0.1:8888 +- Now run the backend on DroneWorld and navigate to the below address and you would be able to see PixelStreaming working +> http://localhost:8888 diff --git a/readme.md b/readme.md index 08b7d7ad6..0bb74187a 100644 --- a/readme.md +++ b/readme.md @@ -1,105 +1,423 @@ -# DroneReqValidator +# DroneWorld -DroneReqValidator (DRV) is a complete Drone simulation ecosystem that automatically generates realistic environments, monitors Drone activities against predefined safety parameters, and generates detailed acceptance test reports for effective debugging and analysis of Drone software applications. +## Overview -# DroneWis +**Drone World**, a key component of DRV, is an advanced simulation platform for testing small unmanned aerial systems (sUAS). It enables users to configure detailed test scenarios by specifying: -DroneWis is a CFD-based wind simulation component that is integrated with DroneReqValidator. It is used to automatically simulate the wind flow in the environment. +- **Environmental Conditions:** Weather, terrain, and other environmental factors. +- **sUAS Capabilities:** Sensors, hardware configurations, and other drone specifications. +- **Mission Objectives:** Specific goals and tasks for each simulation. -## DRV + DroneWis +The platform generates a realistic 3D simulation environment, monitors data to ensure safety, detects issues, and produces comprehensive test reports with detailed analysis. By automating and streamlining the testing process, Drone World enhances safety, reliability, and efficiency for drone developers. It allows for comprehensive pre-flight testing in ultra-realistic environments, helping developers refine their systems and iterate more rapidly on complex missions. Our team at OSS is dedicated to continuously enhancing Drone World's capabilities, including refining environmental settings, drone configurations, and integrating new features. -1. Switch to the branch `ASE-docker` +### Wiki -2. Use the docker compose.yaml file to run the complete system. - ```bash - docker-compose up - ``` -3. Then download DRV_1.0.0 from the [link](https://sluedu-my.sharepoint.com/:f:/g/personal/ankit_agrawal_1_slu_edu/ElbD1q-O8fBFgGDqov6Mh5EBsJ90YyPj2fzsIznTP6AX-w?e=XZaPiX) +Check out our [Wiki](https://github.com/oss-slu/DroneWorld/wiki) for detailed and important information. It's constantly being updated to provide you with the latest resources and insights. -4. Unzip and run the Blocks.exe file to start the simulation. +## DroneReqValidator -now you can interface the ecosystem using the UI at http://localhost:3000 +**DroneReqValidator (DRV)** is a comprehensive drone simulation ecosystem that automatically creates realistic environments, monitors drone activity against predefined safety parameters, and produces detailed acceptance test reports for efficient debugging and analysis of drone software applications. Check out a [demo video](https://www.youtube.com/watch?v=Fd9ft55gbO8) showcasing DRV in action. +## System Requirements -## Demo +### Docker Deployment (Recommended) -- [DRV demo video](https://www.youtube.com/watch?v=Fd9ft55gbO8) -- [DroneWiS demo video](https://youtu.be/khBHEBST8Wc) +- **Docker** and **Docker Compose** +- **macOS** (Apple Silicon or Intel) / **Linux** / **Windows with WSL2** +- 8GB+ RAM recommended +- 20GB+ available disk space + +### Traditional Deployment -## System requirement - Windows 10/11 -- Unreal engine 5.0 (optional) - Python 3.10 -- node.js -- npm - +- Node.js -## Usage +## Architecture DroneReqValidator has 3 main components: -1. Unreal application -2. Python backend -3. React frontend - -To use the DroneReqValidator, follow the steps based on which OS you are working with: -#### [Windows](docs/windowsinstallation.md) -#### [macOS](docs/macinstallation.md) - -## Citation - -If you use this project in your research, please cite our paper: -1. **DroneReqValidator: Facilitating High Fidelity Simulation Testing for Uncrewed Aerial Systems Developers** -```bibtex -@inproceedings{zhang2023dronereqvalidator, title={DroneReqValidator: Facilitating High Fidelity Simulation Testing for Uncrewed Aerial Systems Developers}, -author={Zhang, Bohan and Shivalingaiah, Yashaswini and Agrawal, Ankit}, -booktitle={2023 38th IEEE/ACM International Conference on Automated Software Engineering (ASE)}, -pages={2082--2085}, year={2023}, - organization={IEEE} } -``` - -2. **A Requirements-Driven Platform for Validating Field Operations of Small Uncrewed Aerial Vehicles** - -```bibtex -@inproceedings{agrawal2023requirements, - title={A Requirements-Driven Platform for Validating Field Operations of Small Uncrewed Aerial Vehicles}, - author={Agrawal, Ankit and Zhang, Bohan and Shivalingaiah, Yashaswini and Vierhauser, Michael and Cleland-Huang, Jane}, - booktitle={2023 IEEE 31st International Requirements Engineering Conference (RE)}, - pages={29--40}, - year={2023}, - organization={IEEE} - } + +1. **DRV-Unreal** - Unreal-based simulation engine (headless mode) +2. **Flask Backend** - Python-based simulation controller and monitoring service +3. **React Frontend** - JavaScript-based user interface for configuration and visualization + +## Quick Start (Docker) + +### Prerequisites + +Ensure Docker and Docker Compose are installed on your system. + +### GitHub Token (Required for Simulator) + +The simulator (`drv-unreal`) requires a GitHub Personal Access Token to build. If you're only working on frontend/backend, you can skip this step. See setup instructions in [Troubleshooting](#set-up-github-token). + +### Using Helper Scripts (Recommended) + +**Linux/macOS:** + +```bash +# First time: Set GitHub token (only needed for simulator) +./dev.sh token + +# Development mode (frontend + backend only) +./dev.sh dev + +# Full stack (includes simulator) +./dev.sh full + +# View logs +./dev.sh logs + +# Stop services +./dev.sh stop +``` + +**Windows (PowerShell):** + +```powershell +# First time: Set GitHub token (only needed for simulator) +.\dev.ps1 token + +# Development mode (frontend + backend only) +.\dev.ps1 dev + +# Full stack (includes simulator) +.\dev.ps1 full + +# View logs +.\dev.ps1 logs + +# Stop services +.\dev.ps1 stop +``` + +Run `./dev.sh help` or `.\dev.ps1 help` to see all available commands. + +### Option 1: Full Stack (Recommended for Testing) + +Run all services including the simulation engine: + +```bash +./dev.sh full # Linux/macOS +.\dev.ps1 full # Windows + +# Or directly: +docker-compose up +``` + +**Services started:** + +- Frontend UI (http://localhost:3000) +- Backend API (http://localhost:5000) +- Simulation Engine (http://localhost:3001) +- Storage services + +### Option 2: Frontend/Backend Only (Recommended for Development) + +Run without the simulation engine for faster development: + +```bash +./dev.sh dev # Linux/macOS +.\dev.ps1 dev # Windows + +# Or directly: +docker-compose -f docker-compose.dev.yml up ``` -3. **DroneWiS: Automated Simulation Testing of small Unmanned Aerial System in Realistic Windy Conditions** - ```bibtex - @inproceedings{10.1145/3691620.3695351, - author = {Zhang, Bohan and Agrawal, Ankit}, - title = {DroneWiS: Automated Simulation Testing of small Unmanned Aerial System in Realistic Windy Conditions}, - year = {2024}, - isbn = {9798400712487}, - publisher = {Association for Computing Machinery}, - address = {New York, NY, USA}, - url = {https://doi.org/10.1145/3691620.3695351}, - doi = {10.1145/3691620.3695351}, - abstract = {The continuous evolution of small Unmanned Aerial Systems (sUAS) demands advanced testing methodologies to ensure their safe and reliable operations in the real-world. To push the boundaries of sUAS simulation testing in realistic environments, we previously developed the DroneReqValidator (DRV) platform [11], allowing developers to automatically conduct simulation testing in digital twin of earth. In this paper, we present DRV 2.0, which introduces a novel component called DroneWiS (Drone Wind Simulation). DroneWiS allows sUAS developers to automatically simulate realistic windy conditions and test the resilience of sUAS against wind. Unlike current state-of-the-art simulation tools such as Gazebo and AirSim that only simulate basic wind conditions, DroneWiS leverages Computational Fluid Dynamics (CFD) to compute the unique wind flows caused by the interaction of wind with the objects in the environment such as buildings and uneven terrains. This simulation capability provides deeper insights to developers about the navigation capability of sUAS in challenging and realistic windy conditions. DroneWiS equips sUAS developers with a powerful tool to test, debug, and improve the reliability and safety of sUAS in real-world. A working demonstration is available at https://youtu.be/khBHEBST8Wc.}, - booktitle = {Proceedings of the 39th IEEE/ACM International Conference on Automated Software Engineering}, - pages = {2358–2361}, - numpages = {4}, - keywords = {testing, environmental factors, unmanned aerial systems}, - location = {Sacramento, CA, USA}, - series = {ASE '24} + +**Services started:** + +- Frontend UI (http://localhost:3000) +- Backend API (http://localhost:5000) +- Storage services + +**Use this when:** + +- Working on frontend UI/UX +- Developing backend API endpoints +- Testing frontend ↔ backend integration +- You don't have access to build the simulator +- You want faster startup times + +### Start Individual Services + +```bash +./dev.sh frontend # Frontend only +./dev.sh backend # Backend only +./dev.sh simulator # Simulator only + +# Or directly: +docker-compose up frontend +docker-compose up backend +docker-compose up drv-unreal +``` + +### Viewing Logs + +**Using helper scripts:** + +```bash +./dev.sh logs # Linux/macOS +.\dev.ps1 logs # Windows +``` + +**Using docker-compose directly:** + +```bash +docker-compose -f docker-compose.dev.yml logs -f frontend backend +``` + +### Stopping Development Services + +**Using helper scripts:** + +```bash +./dev.sh stop-dev # Linux/macOS +.\dev.ps1 stop-dev # Windows +``` + +**Using docker-compose directly:** + +```bash +docker-compose -f docker-compose.dev.yml down +``` + +### Configuration + +1. Configure AirSim settings in `config/airsim/`: + - `settings.json` - Drone and simulation configuration + - `cesium.json` - Geographic coordinates for terrain generation + +Example `settings.json`: + +```json +{ + "SettingsVersion": 1.2, + "SimMode": "Multirotor", + "Vehicles": { + "Drone1": { + "FlightController": "SimpleFlight", + "X": 0, + "Y": 0, + "Z": 0 + } } - ``` +} +``` -## Contributing +Example `cesium.json`: + +```json +{ + "latitude": 38.63657, + "longitude": -90.236895, + "height": 163.622131 +} +``` + +## Deployment + +### Start All Services + +```bash +docker-compose up +``` + +### Start Individual Services -We welcome contributions to this project. To contribute, please follow these steps: +```bash +# Simulation engine only +docker-compose up drv-unreal + +# Backend only +docker-compose up backend + +# Frontend only +docker-compose up frontend +``` + +### Access the Application + +- **Frontend UI**: http://localhost:3000 +- **Backend API**: http://localhost:5000 +- **Backend Health Check**: http://localhost:5000/api/health +- **Simulation Engine API**: http://localhost:3001 +- **Simulation Engine PixelStream**: http://localhost:8888 + +## Architecture Notes + +### Headless Simulation + +The DRV-Unreal simulation engine runs in **headless mode** using the `-nullrhi` flag, which: + +- Bypasses GPU rendering requirements +- Enables deployment on servers without graphics hardware +- Works on Apple Silicon Macs via QEMU emulation +- Reduces resource consumption while maintaining physics simulation + +### Network Communication + +- The simulation engine exposes AirSim's API on port 3001 +- Backend communicates with the simulation engine via this TCP connection +- Frontend communicates with backend via REST API + +## Development + +### Building Custom Images + +```bash +# Build DRV-Unreal simulation engine image +docker-compose build drv-unreal + +# Build backend image +docker-compose build backend + +# Build frontend image +docker-compose build frontend +``` + +### Viewing Logs + +```bash +# All services +docker-compose logs -f + +# Specific service +docker-compose logs -f drv-unreal +docker-compose logs -f backend +docker-compose logs -f frontend +``` + +### Stopping Services + +```bash +# Stop all services +docker-compose down + +# Stop and remove volumes +docker-compose down -v +``` +### Hot Reload + +Both frontend and backend support automatic hot reload during development: + +- **Frontend**: Changes to files in `frontend/src/` trigger automatic recompilation +- **Backend**: Changes to Python files automatically restart the Flask server + +Verify hot reload is working: +```bash +# Watch logs for recompilation/restart messages +docker-compose logs -f frontend backend +``` + +For detailed development workflows and contribution guidelines, see our [Contributing Guide](https://github.com/oss-slu/DroneWorld/wiki/Contributing-Guide). + +## Traditional Usage (Non-Docker) + +To begin using DroneReqValidator with traditional installation, refer to our [Getting Started](https://github.com/oss-slu/DroneWorld/wiki/Getting-Started) guide. + +## Troubleshooting + +### Sample `.env` Files + +The contents of `.env` might include the following variables: + +```sh +GITHUB_TOKEN=ghp_xxxxxxx +``` + +The contents of `./sim/.env` might include the following variables: + +```sh +GITHUB_TOKEN=ghp_xxxxxxx +``` + +The contents of `./backend/.env` should include the following variables: + +```sh +# Storage Configuration +STORAGE_TYPE=gcs +GCS_BUCKET_NAME=droneworld +GDRIVE_FOLDER_ID=your_folder_id_here + +# Credentials +GCS_CREDENTIALS_PATH=/app/credentials/gcs-key.json +GDRIVE_CREDENTIALS_PATH=/app/credentials/gdrive-key.json + +# External APIs +GOOGLE_MAPS_API_KEY=your_api_key_here + +# Wind Service Configuration +WIND_SERVICE_HOST=hostname.or.ip.address +WIND_SERVICE_PORT=5001 + +# Storage Emulator Host +STORAGE_EMULATOR_HOST=localhost:4443 + +``` + +The contents of `./frontend/.env` should include the following variables: + +```sh +REACT_APP_DEMO_USER_EMAIL='name@domain.tld' +REACT_APP_CESIUM_ION_ACCESS_TOKEN='yaddayaddayadda' +``` + +### Set Up GitHub Token + +**Linux/macOS:** +```bash +./dev.sh token +# Enter your token when prompted +# Token is automatically saved and loaded for future sessions +``` + +**Windows (PowerShell):** +```powershell +.\dev.ps1 token +# Enter your token when prompted +# Token is automatically saved and loaded for future sessions +``` + +**Creating a GitHub Personal Access Token:** +1. Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic) +2. Click "Generate new token (classic)" +3. Give it a descriptive name (e.g., "DroneWorld Simulator") +4. Select scopes: `repo` (Full control of private repositories) +5. Click "Generate token" +6. Copy the token and use it with the `token` command above + +**Note:** Once saved, the token is automatically loaded from `.env` when you run `./dev.sh full` or `./dev.sh simulator`. + +### Port Already in Use +If you see errors about ports 3000, 3001, or 5000 already being in use: +```bash +# Stop conflicting services or change ports in docker-compose.yml +docker-compose down +``` + +### Simulation Engine Not Responding + +Check logs for initialization: + +```bash +docker logs drv-unreal-1 | grep -E "LogNet|LogWorld|Server" +``` + +Look for messages like: + +- `LogWorld: Bringing World ... up for play` +- Server initialization complete + +### Memory Issues + +If the simulation engine container crashes with memory errors, increase Docker's memory limit: + +- **Docker Desktop**: Settings → Resources → Memory (set to 8GB+) +- **Linux**: No limit by default, but ensure system has sufficient RAM + +## Contributing -1. Fork the repository. -2. Create a new branch for your feature or bug fix. -3. Make your changes and commit them with a descriptive message. -4. Push your changes to your fork. -5. Submit a pull request to the main repository. -6. We will review your changes and merge them if they align with the project's goals. +Contributions to this project are welcome! For details on how to contribute, please follow our [Contributing Guide](https://github.com/oss-slu/DroneWorld/wiki/Contributing-Guide). ## License -This project is licensed under the MIT license. See the LICENSE file for more information. +This project is licensed under the MIT license. See the LICENSE file for more information. \ No newline at end of file diff --git a/sim/Dockerfile b/sim/Dockerfile new file mode 100644 index 000000000..0aedde097 --- /dev/null +++ b/sim/Dockerfile @@ -0,0 +1,127 @@ +# Stage 1: Download and extract the binary +FROM ubuntu:22.04 AS downloader + +# Prevent interactive prompts during installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install download tools +RUN apt-get update && apt-get install -y \ + wget \ + unzip \ + ca-certificates \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /tmp/download + +# Download the DRV-Unreal Linux binary from GitHub releases +ARG VERSION=v2.0.0 +ARG GITHUB_REPO=UAVLab-SLU/DRV-Unreal +ARG GITHUB_TOKEN + +# Download and extract the Linux release +# For private repos, must use GitHub API asset endpoint +RUN if [ -n "$GITHUB_TOKEN" ]; then \ + echo "Downloading from private repo via GitHub API..." && \ + # Get the asset ID for Linux.zip using jq + ASSET_ID=$(wget -qO- \ + --header="Authorization: Bearer ${GITHUB_TOKEN}" \ + "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${VERSION}" | \ + jq -r '.assets[] | select(.name=="Linux.zip") | .id') && \ + echo "Found Linux.zip asset ID: $ASSET_ID" && \ + # Download using API endpoint (required for private repos) + wget --header="Authorization: Bearer ${GITHUB_TOKEN}" \ + --header="Accept: application/octet-stream" \ + "https://api.github.com/repos/${GITHUB_REPO}/releases/assets/$ASSET_ID" \ + -O linux.zip; \ + else \ + echo "Downloading from public repo..." && \ + wget https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/Linux.zip \ + -O linux.zip; \ + fi \ + && echo "Download complete, extracting..." \ + && unzip -q linux.zip -d /tmp/extract \ + && rm linux.zip \ + && echo "Searching for Blocks.sh..." \ + && BLOCKS_DIR=$(find /tmp/extract -name "Blocks.sh" -type f | head -1 | xargs dirname) \ + && echo "Found in: $BLOCKS_DIR" \ + && echo "Contents:" \ + && ls -la "$BLOCKS_DIR" \ + && echo "Moving contents to /app..." \ + && cp -r "$BLOCKS_DIR"/. /app/ \ + && rm -rf /tmp/extract \ + && echo "Files in /app:" \ + && ls -la /app/ | head -20 + +# Make the binary executable +RUN chmod +x /app/Blocks.sh + +# Stage 2: Runtime environment with Vulkan drivers +# Force x86_64 platform for Apple Silicon compatibility +FROM --platform=linux/amd64 ubuntu:22.04 AS runtime + +# Prevent interactive prompts during installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install Vulkan drivers and ALL runtime dependencies for Unreal Engine +# Including 32-bit libraries that might be needed +RUN apt-get update && apt-get install -y \ + mesa-vulkan-drivers \ + vulkan-tools \ + libvulkan1 \ + libgl1 \ + libglu1-mesa \ + libxrandr2 \ + libxinerama1 \ + libxcursor1 \ + libxi6 \ + libc6 \ + libstdc++6 \ + libgcc-s1 \ + zlib1g \ + libx11-6 \ + libxext6 \ + libxrender1 \ + libxtst6 \ + libxau6 \ + libxdmcp6 \ + libxcb1 \ + libasound2 \ + libpulse0 \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user for running Unreal Engine +RUN useradd -m -s /bin/bash -u 1000 ue4 && \ + echo "ue4 ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + + +# Set working directory +WORKDIR /app + +# Copy the extracted binary from downloader stage +COPY --from=downloader /app /app + +# Change ownership to non-root user +RUN chown -R ue4:ue4 /app + +# Verify Vulkan installation (optional, for debugging) +RUN vulkaninfo --summary || echo "Vulkan info check complete" + +RUN mkdir -p /home/ue4/Documents/AirSim && \ + touch /home/ue4/Documents/AirSim/settings.json && \ + touch /home/ue4/Documents/AirSim/cesium.json && \ + ln -sf /home/ue4/Documents/AirSim/settings.json /home/ue4/Documents/AirSim\\settings.json && \ + ln -sf /home/ue4/Documents/AirSim/cesium.json /home/ue4/Documents/AirSim\\cesium.json + +# Switch to non-root user +USER ue4 + +# Expose ports +# Port 3000 for the UI (from DRV docs) +# Port 8888 for PixelStreaming (if using that feature) +EXPOSE 3000 8888 + +# Default command - run with graphicsadapter flag as shown in docs +CMD ["./Blocks.sh", "-RenderOffscreen", "-graphicsadapter=0"] \ No newline at end of file diff --git a/sim/README.md b/sim/README.md new file mode 100644 index 000000000..4357c96d9 --- /dev/null +++ b/sim/README.md @@ -0,0 +1,197 @@ +# DRV-Unreal Docker Setup + +This setup containerizes the DRV-Unreal v2.0.0 Linux binary from GitHub releases for local development using a multi-stage build for optimal image size. + +## Prerequisites + +- Docker +- Docker Compose +- (Optional) NVIDIA GPU with nvidia-docker2 for GPU acceleration + +## Architecture + +This setup uses a **multi-stage Docker build**: + +1. **Stage 1 (downloader)**: Downloads and extracts the 1.27 GB `Linux.zip` +2. **Stage 2 (runtime)**: Clean Ubuntu environment with only runtime dependencies + - Vulkan drivers (mesa-vulkan-drivers, libvulkan1, vulkan-tools) + - Graphics libraries (libgl1, libglu1-mesa) + - X11 libraries (libxrandr2, libxinerama1, libxcursor1, libxi6) + +This approach keeps the final image clean without build tools like wget and unzip. + +## What's Included + +This setup downloads and runs the DRV-Unreal v2.0.0 Linux build which includes: + +- Aurelia drone model with React-G GPS receiver +- Unreal Engine 5.5 +- Raytracing and DLSS support +- PixelStreaming capability +- Vulkan driver support + +## Quick Start + +### 1. Build and Run + +```bash +# Build the image (downloads ~1.27 GB Linux.zip) +docker-compose build + +# Start the service +docker-compose up -d + +# View logs +docker-compose logs -f drv-unreal + +# Stop the service +docker-compose down +``` + +### 2. Access the Application + +The DRV ecosystem UI should be available at + +## Configuration Options + +### GPU Support (Recommended for Unreal Engine) + +If you have an NVIDIA GPU, uncomment the GPU section in `docker-compose.yml`: + +```yaml +deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] +``` + +Then ensure you have nvidia-docker2 installed: + +```bash +# Install nvidia-docker2 +distribution=$(. /etc/os-release;echo $ID$VERSION_ID) +curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - +curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \ + sudo tee /etc/apt/sources.list.d/nvidia-docker.list +sudo apt-get update && sudo apt-get install -y nvidia-docker2 +sudo systemctl restart docker +``` + +### PixelStreaming (Optional) + +To enable PixelStreaming for remote rendering: + +1. You'll need the PixelStreamingInfrastructure.zip from the release +2. Run the signaling server separately +3. Modify the CMD in Dockerfile to include PixelStreaming args: + + ```dockerfile + CMD ["./Blocks.sh", "-AudioMixer", "-PixelStreamingIP=localhost", "-PixelStreamingPort=8888"] + ``` + +### Custom Arguments + +To run with different arguments, override the command: + +```bash +docker-compose run drv-unreal ./Blocks.sh -graphicsadapter=0 +``` + +Or modify the `command` in docker-compose.yml: +```yaml +command: ["./Blocks.sh", "-graphicsadapter=1", "-YourCustomArg"] +``` + +## Troubleshooting + +### Verify Vulkan Installation + +The Dockerfile includes a Vulkan verification step during build. To manually verify: + +```bash +# Check if Vulkan is available in the container +docker-compose exec drv-unreal vulkaninfo + +# Or check the summary +docker-compose exec drv-unreal vulkaninfo --summary +``` + +Expected output should show available Vulkan drivers and devices. + +### Vulkan Driver Issues + +If you see Vulkan-related errors: + +```bash +# Check if Vulkan is available in the container +docker-compose exec drv-unreal vulkaninfo + +# Or run with debug +docker-compose run drv-unreal bash +vulkaninfo +``` + +### Display Issues + +If running with GUI (not headless): + +```bash +# Allow X11 connections (on host) +xhost +local:docker + +# Run with proper DISPLAY variable +DISPLAY=:0 docker-compose up +``` + +### Container Exits Immediately + +Check the logs: + +```bash +docker-compose logs drv-unreal +``` + +If the binary requires X11 or other dependencies, you may need to run it in headless mode or with additional configuration. + +### Debugging + +To keep the container running for debugging: + +```yaml +# In docker-compose.yml, uncomment: +command: tail -f /dev/null +``` + +Then exec into it: + +```bash +docker-compose exec drv-unreal bash +./Blocks.sh -graphicsadapter=1 +``` + +## Architecture + +The setup includes: + +- **drv-unreal service**: Main Unreal application container +- **Exposed ports**: + - 3000: Web UI interface + - 8888: PixelStreaming (if enabled) + +## Notes + +- The Linux.zip file is ~1.27 GB and will be downloaded during build +- First build will take several minutes +- Vulkan drivers are pre-installed in the container +- The application runs with `-graphicsadapter=1` by default + +## Next Steps + +1. Build and start: `docker-compose up -d` +2. Check logs: `docker-compose logs -f` +3. Access UI: `http://localhost:3000` +4. Configure GPU support if needed +5. Integrate with your other services in the same docker-compose.yml \ No newline at end of file diff --git a/start_enviroment.ps1 b/start_enviroment.ps1 new file mode 100644 index 000000000..8f3fc766d --- /dev/null +++ b/start_enviroment.ps1 @@ -0,0 +1,57 @@ + +#security permissions for npm start, if you dont allow just type "npm start" after the script runs + +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +function Write-Info($msg) { + Write-Host "`n[INFO] $msg`n" -ForegroundColor Cyan +} + +#create or check if airsim doc is existing +$documentsPath = [Environment]::GetFolderPath('MyDocuments') +$airsimPath = Join-Path $documentsPath "AirSim" + +if (-Not (Test-Path $airsimPath)) { + New-Item -ItemType Directory -Path $airsimPath | Out-Null + Write-Host "Created AirSim folder at: $airsimPath" -ForegroundColor Green +} else { + Write-Host "AirSim folder already exists at: $airsimPath" -ForegroundColor Yellow +} + + + +#check if venv is made, otherwise do the whole install +if (-Not (Test-Path "venv")) { + Write-Host "Creating virtual enviroment 'venv' " -ForegroundColor Green + py -3.10 -m venv venv +} else { + Write-Host "Found exisitng virtual enviroment 'venv' " -ForegroundColor Yellow +} + +.\venv\Scripts\Activate.ps1 + +Set-Location -Path ".\backend" + +Write-Info "installing requirements..." + +Start-Process powershell -ArgumentList "-NoExit", "-Command", "cd $(Get-Location); " + +pip install -r requirements.txt + + +#back end starting +Write-Info "Starting backend..." + + + +#open another shell for backend +Start-Process powershell -ArgumentList "-NoExit", "-Command", "cd $(Get-Location)\PythonClient\server; py simulation_server.py" + +# front end starting +Write-Info "Starting frontend..." +Set-Location -Path "..\frontend" + +npm install + +#open another shell for frontend +Start-Process powershell -ArgumentList "-NoExit", "-Command", "cd $(Get-Location); npm start" +