diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6d1cc17 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +VITE_CESIUM_ACCESS_TOKEN="YOUR_CESIUM_ACCESS_TOKEN" + +VITE_MOON_PHASE_APP_ID="YOUR_APP_ID" +VITE_MOON_PHASE_APP_SECRET="YOUR_MOON_APP_SECRET" + +OPENSKY_CLIENT_ID="YOUR_CLIENT_ID" +OPENSKY_CLIENT_SECRET="YOU_SECRET" + +GMAIL_APP_PASSWORD="YOUR_GMAIL_APP_PASSWORD" diff --git a/.gitignore b/.gitignore index a3f7a51..64ed0f2 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,8 @@ coverage *.sw? *.tsbuildinfo - .eslintcache - +.env # Cypress /cypress/videos/ /cypress/screenshots/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index ab1f416..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/Planium.iml b/.idea/Planium.iml deleted file mode 100644 index c956989..0000000 --- a/.idea/Planium.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml deleted file mode 100644 index d23208f..0000000 --- a/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index f288451..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index a7cea0b..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["Vue.volar"] -} diff --git a/API/fetchFlights.py b/API/fetchFlights.py new file mode 100644 index 0000000..5cc2704 --- /dev/null +++ b/API/fetchFlights.py @@ -0,0 +1,113 @@ +""" +Author : Sofian Hussein +Date : 14.01.2026 +Project : FastAPI Flights Backend +Desc : API data fetching script +""" +import requests +from os import path, getenv +from dotenv import load_dotenv +from fastapi import HTTPException + +dotenv_path = path.join(path.dirname(__file__), '..', '.env') +load_dotenv(dotenv_path) + + +def get_opensky_token(): + client_id = getenv("OPENSKY_CLIENT_ID") + client_secret = getenv("OPENSKY_CLIENT_SECRET") + + auth_url = "https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token" + + payload = { + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret + } + + try: + response = requests.post(auth_url, data=payload) + response.raise_for_status() + + token_data = response.json() + return token_data.get("access_token") + + except requests.exceptions.RequestException as e: + print(f"Erreur lors de l'authentification : {e}") + return None + + +def get_flights(user_lat, user_long): + """ + Fetch flight data from OpenSky Network API for Switzerland region + + Args: + user_lat: User latitude in degrees + user_long: User longitude in degrees + + Returns: + List of dictionaries containing flight data + + Example: + [{'origin_country': 'Portugal', 'lat': 46.8, 'long': 7.0111, 'alt': 10.51, 'velocity': 196.84, + 'heading': 231.15, 'vertical_rate': 2.28},...] + + Field Descriptions: + "origin_country": country of origin of the plane + "lat": plane latitude (°) -90 to 90 + "long": plane longitude (°) -180 to 180 + "alt": plane altitude (m) + "velocity": plane speed (m/s) + "heading": plane heading (°) 0 to 360, true north + "vertical_rate": Vertical speed (m/s) + """ + url = "https://opensky-network.org/api/states/all" + + """ + Compute the bounding box for fetching flight data from the OpenSky Network API + based on the user's location. The goal is to define a rectangular region that + covers the visible sky towards the south from the user's position. + + In this example, we use Ste-Croix as a reference point, + Geneva Airport is excluded from the area to avoid having aircraft on the ground. + """ + params = { + "lamin": user_lat-0.52, + "lomin": user_long-1, + "lamax": user_lat+1, + "lomax": user_long+1 + } + token = get_opensky_token() + # search for data at the URL specified with the parameters + response = requests.get(url, params=params, headers={'Authorization': 'Bearer ' + token}) + + if response.status_code != 200: + if response.status_code == 429: + raise HTTPException(status_code=429, detail="Item not found") + return response + # extract only the JSON of the response + data = response.json() + + if not data.get("states"): + return [] + + # list that contains all flights + flights = [] + + # loop that iterates through the json + for state in data["states"]: + # creating a dictionary that stores only useful data + flight = { + "id": state[1], + "origin_country": state[2], + "lat": state[6], + "long": state[5], + "alt": state[7], + "velocity": state[9], + "heading": state[10], + "vertical_rate": state[11] + } + # add the flight to the list of aircraft + flights.append(flight) + + return flights diff --git a/API/fetchMoon.py b/API/fetchMoon.py new file mode 100644 index 0000000..daa5ea9 --- /dev/null +++ b/API/fetchMoon.py @@ -0,0 +1,179 @@ +import math + +import numpy as np +import requests +from requests.adapters import HTTPAdapter +from urllib3 import Retry + + +def get_moon_data(start_time, stop_time, step): # src: https://ssd-api.jpl.nasa.gov/doc/horizons.html + """ + Fetch Moon's RA, DEC and phase from Horizon API + + :param start_time: min time value for moon fetching (start) + :param stop_time: max time value for moon fetching (end) + :param step: time step for moon fetching (hours) + :return: dict containing list of moon's data at specific times + """ + url = 'https://ssd.jpl.nasa.gov/api/horizons.api' + + API_fetch_params = { + 'format': 'json', + 'COMMAND': "'301'", # Moon + 'OBJ_DATA': 'NO', + 'MAKE_EPHEM': 'YES', + 'EPHEM_TYPE': 'OBSERVER', + 'CENTER': "'500@399'", # Earth's center coordinates + 'START_TIME': f"'{start_time}'", # From 'YYYY-MM-DD' + 'STOP_TIME': f"'{stop_time}'", # To 'YYYY-MM-DD' + 'STEP_SIZE': f"'{step}'", # Step (ex.'1h') + 'QUANTITIES': "'1,9'", # 1 = RA/DEC, Quantity 9 = Brightness/Phase + } + + # setup retries to handle SSLError or connection drops + # src: https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html + # src: https://requests.readthedocs.io/en/latest/user/advanced/ + session = requests.Session() + retries = Retry( + total=5, # if a request fails, try 5 more times + backoff_factor=1, # wait between tries to prevent overloading the system + status_forcelist=[502, 503, 504] # only retry if server returns 502, 503, 504 error status (bad gateway/service unavailable) + ) + # apply for any url 'http://' + session.mount('http://', HTTPAdapter(max_retries=retries)) + + try: + response = requests.get( + url, + params=API_fetch_params, # pass api's custom params + timeout=15 # trigger retry if server doesn't respond in 15s + ) + response.raise_for_status() # raise Exception for any error + data = response.json() # parse json response + except Exception as e: + print(e) + return [] + + # check if 'result' key exists + if not data.get('result'): + return [] + + result = data.get('result', '') + # check if data keyword 'SOE' was returned + if '$$SOE' not in result: + return [] + + lines = data.get('result', '').split('\n') # Split text in lines + + moon_data = [] # dic with data to return + + is_data_zone = False # data zone flag + for line in lines: + if '$$SOE' in line: # start of data + is_data_zone = True + continue + if '$$EOE' in line: # end of data + is_data_zone = False + break + + if is_data_zone: + # ex: 2026-Jan-15 10:00 16 59 59.11 -27 49 59.3 -7.374 6.202\n + values = [] + for v in line.split(): # loop through split values + if v.strip(): # only keep if it's not an empty string + values.append(v) + + # NASA column mapping for Quantities 1,9: + # values[0,1] = Date, Time + # values[2,3,4] = RA (h, m, s) + # values[5,6,7] = DEC (d, m, s) + # values[8] = APmag + # values[9] = S-br + # values[10] = Illu% (Phase) + + if len(values) >= 10: + try: + # Convert RA (sexagesimal) to decimal + ra_decimal = ( + float(values[2]) # h (hours) + + float(values[3])/60 # min to h + + float(values[4])/3600 # seconds to h + ) + + # Convert DEC (sexagesimal) to decimal + # handle negative DEC for decimal addition + if "-" in values[5]: + sign=-1 + else: + sign=1 + + dec_decimal = ( + abs(float(values[5])) # d (degrees) absolute value + + float(values[6])/60 # arcminutes to d + + float(values[7])/3600 # arcseconds to d + ) + + dec_decimal *= sign # apply sign + + # structure return data + entry = { + 'datetime': f"{values[0]} {values[1]}", # 'YYYY-MM-DD HH:SS' + 'ra': round(ra_decimal, 6), # 6 Decimal Right Ascension (hours) + 'dec': round(dec_decimal,6), # 6 Decimal Declination (degrees) + 'phase': float(values[-1]) # Phase decimal percentage (0.00 - 100.00) + } + moon_data.append(entry) + except Exception as e: + print(f"Error: {e}") + continue + return moon_data + +def get_cesium_moon_coordinates(moon_data, altitude_m): + """ + Converts RA/Dec to X, Y, Z coordinates + :param moon_data: datetime, ra, dec, phase + :param altitude_m: altitude above Earth's surface in meters + :return: dict with timestamp, X, Y, Z coordinates and moon phase + """ + # Earth radius in meters + earth_radius = 6371000 + # total distance from Earth's center to Moon + r = earth_radius + altitude_m + + cesium_coordinates = [] + + for entry in moon_data: + # convert moon coordinates to radians src: src: https://skyandtelescope.org/astronomy-resources/right-ascension-declination-celestial-coordinates/ + # RA [hours] to degrees + ra_deg = entry['ra'] * 15 + # RA [degrees] to radians + ra_rad = math.radians(ra_deg) + # Dec [degrees] to radians + dec_rad = math.radians(entry['dec']) + + # convert spherical to cartesian src: https://mathworld.wolfram.com/SphericalCoordinates.html + # x = r * cos(dec) * cos(ra) + # y = r * cos(dec) * sin(ra) + # z = r * sin(dec) + x = r * math.cos(dec_rad) * math.cos(ra_rad) + y = r * math.cos(dec_rad) * math.sin(ra_rad) + z = r * math.sin(dec_rad) + + cesium_coordinates.append({ + 'timestamp': entry['datetime'], + 'x': round(x, 6), + 'y': round(y, 6), + 'z': round(z, 6), + 'phase': entry['phase'] + }) + + return cesium_coordinates + +""" +# test +raw = get_moon_data('2026-01-15', '2026-01-20', '1h') +if raw: + cesium_coordinates = get_cesium_moon_coordinates(raw, 10000) + print(f"Sample Coord: {cesium_coordinates[0]}") + print(f"Total points: {len(cesium_coordinates)}") +""" diff --git a/API/gmail.py b/API/gmail.py new file mode 100644 index 0000000..ab8ac6b --- /dev/null +++ b/API/gmail.py @@ -0,0 +1,54 @@ +import smtplib +import os +from email.message import EmailMessage +from fastapi import APIRouter, HTTPException, Body +from models.EmailRequest import EmailRequest + +router = APIRouter() + + +@router.post("/send-email") +def send_email(request: EmailRequest): + mail_host = "smtp.gmail.com" # mail manager host + port = 587 # standard SMTP submission port (STARTTLS) + sender_email = "zzabcmail123@gmail.com" # email address that will send mail + sender_password = os.getenv("GMAIL_APP_PASSWORD") # gmail app password + recipient_email = request.user_email # email address that will receive mail + content = request.content + + # compose email + msg = EmailMessage() # init email obj + msg["Subject"] = "Planium : A plane is flying near the Moon 🌙"# email subject + msg["From"] = sender_email # sender + msg["To"] = recipient_email # recipient + msg.set_content(content) # email content + + # send email + with smtplib.SMTP(mail_host, port) as smtp: + smtp.starttls() + smtp.login(sender_email, sender_password) # login with user credentials + smtp.send_message(msg) + +# send test email +@router.post("/send-test-email") +def send_test_email(request: EmailRequest): + sender_email = "zzabcmail123@gmail.com" + sender_password = os.getenv("GMAIL_APP_PASSWORD") + + if not sender_password: + raise HTTPException(status_code=500, detail="GMAIL_APP_PASSWORD not set") + + msg = EmailMessage() + msg["Subject"] = "Planium : A plane is flying near the Moon 🌙" + msg["From"] = sender_email + msg["To"] = request.user_email + msg.set_content("A plane is about to fly near the moon.") + + try: + with smtplib.SMTP("smtp.gmail.com", 587) as smtp: + smtp.starttls() + smtp.login(sender_email, sender_password) + smtp.send_message(msg) + return {"success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/API/main.py b/API/main.py new file mode 100644 index 0000000..06658d5 --- /dev/null +++ b/API/main.py @@ -0,0 +1,96 @@ +""" +Author : Sofian Hussein +Date : 15.01.2026 +Project : FastAPI Flights Backend +Description: Backend service to fetch and serve live flight data using OpenSky API. +""" +import uvicorn +import os +from fastapi import FastAPI, Query, HTTPException +from fetchFlights import get_flights +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from gmail import router as email_router +from models.Log import Log + +app = FastAPI() + +origins = [ + "http://localhost:5173", + "http://127.0.0.1:5173", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +app.include_router(email_router, prefix="/api") + +@app.get("/flights") +def get_flights_endpoint( + lat: float = Query(None, description="User latitude"), + long: float = Query(None, description="User longitude") +): + """ + route that allows planes to be recovered + + :param lat: latitude of the user + :param long: longitude of the user + :return: a dict of all the flights + """ + # Default to Ste-Croix if no coordinates provided + if lat is None or long is None: + lat, long = 46.82, 6.5 + + # Fetch flights for the given coordinates + flights = get_flights(lat, long) + # If no data is found + if not flights: + return {"message": "No data found"} + + return flights + +@app.post("/logs") +def write_logs(log : Log): + """ + Route that writes logs to a file + + :return: "message": "Logs were added" + """ + # Source : https://www.docstring.fr/formations/faq/fichiers/comment-lire-et-ecrire-dans-un-fichier-en-python/ + # Source : www.geeksforgeeks.org/python/create-a-directory-in-python/ + # Source : https://fastapi.tiangolo.com/tutorial/body/ + + # Create a folder at the root + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + logs_dir = os.path.join(project_root, "Logs") + os.makedirs(logs_dir, exist_ok=True) + + log_file_path = os.path.join(logs_dir, "log.txt") + with open(log_file_path, "a", encoding="utf-8") as log_file: + log_file.write(f"{log.message}\n") + log_file.flush() + os.fsync(log_file.fileno()) + return {"message": "Logs were added"} + + +@app.get("/") +def read_root(): + """ + Default route + + :return: "message": "FastAPI Flights Backend Running" + """ + return {"message": "FastAPI Flights Backend Running"} + + +""" +Source Claude : How to change the listening port with Fastapi ? +""" +uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/API/models/EmailRequest.py b/API/models/EmailRequest.py new file mode 100644 index 0000000..5178085 --- /dev/null +++ b/API/models/EmailRequest.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class EmailRequest(BaseModel): + user_email: str + content: str \ No newline at end of file diff --git a/API/models/Log.py b/API/models/Log.py new file mode 100644 index 0000000..bcb85de --- /dev/null +++ b/API/models/Log.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class Log(BaseModel): + message: str \ No newline at end of file diff --git a/API/requirements.txt b/API/requirements.txt new file mode 100644 index 0000000..8412cf9 --- /dev/null +++ b/API/requirements.txt @@ -0,0 +1,44 @@ +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.12.1 +certifi==2026.1.4 +charset-normalizer==3.4.4 +click==8.3.1 +colorama==0.4.6 +dnspython==2.8.0 +email-validator==2.3.0 +fastapi==0.128.0 +fastapi-cli==0.0.20 +fastapi-cloud-cli==0.11.0 +fastar==0.8.0 +h11==0.16.0 +httpcore==1.0.9 +httptools==0.7.1 +httpx==0.28.1 +idna==3.11 +Jinja2==3.1.6 +markdown-it-py==4.0.0 +MarkupSafe==3.0.3 +mdurl==0.1.2 +pydantic==2.12.5 +pydantic-extra-types==2.11.0 +pydantic-settings==2.12.0 +pydantic_core==2.41.5 +Pygments==2.19.2 +python-dotenv==1.2.1 +python-multipart==0.0.21 +PyYAML==6.0.3 +requests==2.32.5 +rich==14.2.0 +rich-toolkit==0.17.1 +rignore==0.7.6 +sentry-sdk==2.50.0 +shellingham==1.5.4 +starlette==0.50.0 +typer==0.21.1 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.6.3 +uvicorn==0.40.0 +watchfiles==1.1.1 +websockets==16.0 \ No newline at end of file diff --git a/README.md b/README.md index d1a3109..7215233 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,47 @@ # Planium - +Planium is a tool for visualizing airplanes, as well as the moon and the sun. +It also allows you to predict 30 seconds before an airplane passes in front of the moon. +Planium makes a rare event accessible: taking a capture of a Moon–aircraft alignment. ## Stacks - [Vue.js](https://vuejs.org/) - [vue-cesium](https://zouyaoji.top/vue-cesium/#/en-US) +- [Cesium](https://cesium.com/cesiumjs/) +- [FastAPI](https://fastapi.tiangolo.com/) - [DaisyUi](https://daisyui.com/) + + +## Requirements +- Node.js 20.19.0+ +- https://nodejs.org/en/download + + +- Python 3.10+ +- https://www.python.org/downloads/ + +## Prerequisites +- [A Cesium ion account](https://ion.cesium.com) (To Downloads and load assets) +- [A Opensky account](https://opensky-network.org) (To get more credits for the API, follow this [tutorial](https://openskynetwork.github.io/opensky-api/rest.html#oauth2-client-credentials-flow)) +- [A Astronomy account](https://astronomyapi.com) (To get the moon phase) +- [A Google app linked to an email](https://myaccount.google.com/apppasswords) (To send emails) + +## Upload non-ion assets +1. Download the following assets and upload them to your ion account: + - [Plane](https://cesium.cdn.prismic.io/cesium/Zv2eybVsGrYSwUFj_Cesium_Air.glb) +2. Go to [your account dashboard](https://ion.cesium.com/assets/?). Drag and drop the model file on this page. +3. Select **3D Model (Convert to glTF)**, then click **Upload**. +4. After it’s done processing, find the **asset ID** by selecting the new asset back in your dashboard and looking under the preview window on the right. + +## Create the .env file +1) Create a .env file at the root of the project. +2) Paste the content of the file ".env.example" +3) Replace the variable values with your own. + ## Project Setup ```sh -npm install +npm run install-all ``` ### Compile and Hot-Reload for Development diff --git a/package-lock.json b/package-lock.json index 87bf38e..b7aa506 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "cesium": "^1.137.0", + "concurrently": "^9.2.1", "vue": "^3.5.26", "vue-cesium": "^3.2.12" }, @@ -2097,6 +2098,30 @@ "resolved": "https://registry.npmjs.org/@zouyaoji/heatmap.js/-/heatmap.js-2.0.8.tgz", "integrity": "sha512-kBQny/zOUFH2OFoVyu6IdGJEcQMENIAASUsaZhk+OuJ9WexsYf6EU2lCyGURcsFly1kTMZKODlV7nBTCgfvJqg==" }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ansis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", @@ -2272,6 +2297,66 @@ "node": ">=20.19.0" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2284,6 +2369,30 @@ "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", "license": "MIT" }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2438,6 +2547,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -2520,7 +2635,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2589,6 +2703,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2602,6 +2725,15 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "license": "MIT" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", @@ -2625,6 +2757,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -3286,6 +3427,15 @@ "quickselect": "^3.0.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -3351,6 +3501,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3361,6 +3520,18 @@ "semver": "bin/semver.js" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -3395,6 +3566,32 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", @@ -3408,6 +3605,21 @@ "node": ">=16" } }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -3470,6 +3682,15 @@ "node": ">=6" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3768,6 +3989,23 @@ "integrity": "sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q==", "license": "MIT" }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", @@ -3784,6 +4022,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -3791,6 +4038,33 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/zrender": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", diff --git a/package.json b/package.json index e8fdee5..f5eac66 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,16 @@ "node": "^20.19.0 || >=22.12.0" }, "scripts": { - "dev": "vite", + "dev": "npx concurrently \"npm run frontend\" \"npm run backend\"", + "frontend": "vite", + "backend": "cd API && (source .venv/bin/activate || .venv\\Scripts\\activate) && python3 main.py || py main.py", + "install-all": "npm install && cd API && py -m venv .venv && .venv\\Scripts\\activate && pip install -r requirements.txt", "build": "vite build", "preview": "vite preview" }, "dependencies": { "cesium": "^1.137.0", + "concurrently": "^9.2.1", "vue": "^3.5.26", "vue-cesium": "^3.2.12" }, diff --git a/src/App.vue b/src/App.vue index e800331..9100670 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,11 +1,12 @@ diff --git a/src/components/Alert.vue b/src/components/Alert.vue new file mode 100644 index 0000000..5713994 --- /dev/null +++ b/src/components/Alert.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/CameraController.vue b/src/components/CameraController.vue new file mode 100644 index 0000000..13defb8 --- /dev/null +++ b/src/components/CameraController.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/components/CompassIndicator.vue b/src/components/CompassIndicator.vue new file mode 100644 index 0000000..dfd8ca1 --- /dev/null +++ b/src/components/CompassIndicator.vue @@ -0,0 +1,130 @@ + + + + + \ No newline at end of file diff --git a/src/components/CoordinateForm.vue b/src/components/CoordinateForm.vue index 9004f38..f4c5663 100644 --- a/src/components/CoordinateForm.vue +++ b/src/components/CoordinateForm.vue @@ -1,27 +1,44 @@ \ No newline at end of file diff --git a/src/components/MoonMiniViewer.vue b/src/components/MoonMiniViewer.vue new file mode 100644 index 0000000..e3b3cf6 --- /dev/null +++ b/src/components/MoonMiniViewer.vue @@ -0,0 +1,231 @@ + + + + + \ No newline at end of file diff --git a/src/components/MoonPhase.vue b/src/components/MoonPhase.vue new file mode 100644 index 0000000..6867ec0 --- /dev/null +++ b/src/components/MoonPhase.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue new file mode 100644 index 0000000..6e12ba4 --- /dev/null +++ b/src/components/NavBar.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/components/Viewer.vue b/src/components/Viewer.vue new file mode 100644 index 0000000..84bf40b --- /dev/null +++ b/src/components/Viewer.vue @@ -0,0 +1,136 @@ + + + + diff --git a/src/components/imagery/Imagery.vue b/src/components/imagery/Imagery.vue new file mode 100644 index 0000000..ed44979 --- /dev/null +++ b/src/components/imagery/Imagery.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/imagery/IonImagery.vue b/src/components/imagery/IonImagery.vue new file mode 100644 index 0000000..a3dd46c --- /dev/null +++ b/src/components/imagery/IonImagery.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/components/mapPreview.vue b/src/components/mapPreview.vue deleted file mode 100644 index 55bb2a7..0000000 --- a/src/components/mapPreview.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - diff --git a/src/components/navigation/MyLocation.vue b/src/components/navigation/MyLocation.vue new file mode 100644 index 0000000..59f51de --- /dev/null +++ b/src/components/navigation/MyLocation.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/components/navigation/Navigation.vue b/src/components/navigation/Navigation.vue new file mode 100644 index 0000000..fa1faaf --- /dev/null +++ b/src/components/navigation/Navigation.vue @@ -0,0 +1,8 @@ + + + diff --git a/src/components/notification/Email.vue b/src/components/notification/Email.vue new file mode 100644 index 0000000..b49dacd --- /dev/null +++ b/src/components/notification/Email.vue @@ -0,0 +1,74 @@ + + + \ No newline at end of file diff --git a/src/components/primitive/Google3dTiles.vue b/src/components/primitive/Google3dTiles.vue new file mode 100644 index 0000000..075ec6a --- /dev/null +++ b/src/components/primitive/Google3dTiles.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/components/primitive/Moon.vue b/src/components/primitive/Moon.vue new file mode 100644 index 0000000..142cb4e --- /dev/null +++ b/src/components/primitive/Moon.vue @@ -0,0 +1,152 @@ + + + diff --git a/src/components/primitive/MoonCenterButton.vue b/src/components/primitive/MoonCenterButton.vue new file mode 100644 index 0000000..7b61fea --- /dev/null +++ b/src/components/primitive/MoonCenterButton.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/components/primitive/Tilesets.vue b/src/components/primitive/Tilesets.vue new file mode 100644 index 0000000..3f24e71 --- /dev/null +++ b/src/components/primitive/Tilesets.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/components/terrain/Terrain.vue b/src/components/terrain/Terrain.vue new file mode 100644 index 0000000..cee8b16 --- /dev/null +++ b/src/components/terrain/Terrain.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/main.js b/src/main.js index e26c3c4..a91ffc7 100644 --- a/src/main.js +++ b/src/main.js @@ -8,5 +8,6 @@ import lang from 'vue-cesium/es/locale/lang/en-us.mjs'; const app = createApp(App) app.mount('#app') app.use(VueCesium,{ + accessToken: import.meta.env.VITE_CESIUM_ACCESS_TOKEN, locale: lang }) diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 0000000..65676dc --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,41 @@ + +export async function getFLights(url, params = {}) { + const searchParams = new URLSearchParams(params); + try { + const response = await fetch(`${url}?${searchParams.toString()}`, { + method: "GET", + }); + + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + + const result = await response.json(); + + return result + } catch (error) { + console.error(error.message); + } +} + +export function getLocation() { + return new Promise((resolve, reject) => { + if (!navigator.geolocation) { + reject(new Error("Geolocation not supported")); + return; + } + + navigator.geolocation.getCurrentPosition( + (position) => { + const coords = { + lat: position.coords.latitude, + long: position.coords.longitude + }; + resolve(coords); + }, + (error) => { + reject(error); + } + ); + }); +} diff --git a/src/utils/camera.js b/src/utils/camera.js new file mode 100644 index 0000000..76c4d36 --- /dev/null +++ b/src/utils/camera.js @@ -0,0 +1,19 @@ +export function flyTo(camera, cesium, lat, lng){ + try { + camera.flyTo({ + destination: cesium.Cartesian3.fromDegrees( + lng ?? 6.500465335539498, // longitude is defaulted to Sainte-Croix if undefined + lat ??46.82166054184684, //latitude is defaulted to Sainte-Croix if undefined + ), + orientation: { + heading: Cesium.Math.toRadians(180.0), // Orientation to the south + pitch: Cesium.Math.toRadians(15.0), + roll: 0.0 + } + }) + } + catch(e) { + console.log(e) + } + +} \ No newline at end of file diff --git a/src/utils/mail.js b/src/utils/mail.js new file mode 100644 index 0000000..70a1b16 --- /dev/null +++ b/src/utils/mail.js @@ -0,0 +1,28 @@ +export async function sendEmail(user_email, content) { + if (!user_email) { + alert("Please enter a valid email address."); + return; + } + + try{ + const res = await fetch("http://localhost:8080/api/send-email", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ user_email: user_email , content: content}), + }); + + if (!res.ok) throw new Error("Failed to send test email"); + + alert("Email sent successfully."); + } catch (error) { + alert("Error sending email:" + error.message); + } +} + +export function saveEmail(email) { + localStorage.setItem("user_email", email); +} + +export function getEmail() { + return localStorage.getItem("user_email"); +} \ No newline at end of file diff --git a/src/utils/scene.js b/src/utils/scene.js new file mode 100644 index 0000000..1022c83 --- /dev/null +++ b/src/utils/scene.js @@ -0,0 +1,480 @@ +import {getFLights} from "@/utils/api.js"; +import {getEmail, sendEmail} from "@/utils/mail.js"; + +// data structure that allows logs to be stored +// Source : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map +const logs = new Map() + +export async function prepareScene(scene){ + //------------Uncomment if performance is low--------------------- + // scene.requestRenderMode = true; + // scene.maximumRenderTimeChange = Infinity; + // scene.globe.maximumScreenSpaceError = 24; + // scene.globe.tileCacheSize = 1000; + // scene.globe.preloadAncestors = false; + // scene.globe.loadingDescendantLimit = 20; + scene.primitives.removeAll(); + scene.contextOptions = { + allowTextureFilterAnisotropic: false, + cameraUnderground: false + } + + + const controller = scene.screenSpaceCameraController; + // Disable all default controls + controller.enableRotate = false; + controller.enableTranslate = false; + controller.enableZoom = false; + controller.enableTilt = false; + controller.enableLook = false; + + controller.lookEventTypes = Cesium.CameraEventType.LEFT_DRAG; + controller.enableLook = true; + // source : https://cesium.com/learn/cesiumjs/ref-doc/Camera.html + // source : https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent + + // minimum FOV degrees in radian + const MIN_FOV = Cesium.Math.toRadians(5); + + // maximum FOV degrees in radian + const MAX_FOV = Cesium.Math.toRadians(100); + + // increment in radian for each step of the mouse + const STEP = Cesium.Math.toRadians(2); + + const canvas = scene.canvas; + + // listen for mouse wheel events + canvas.addEventListener( + "wheel", + (event) => { + event.preventDefault(); + + const camera = scene.camera; + let fov = camera.frustum.fov; + + // increment the FOV + fov += event.deltaY > 0 ? STEP : -STEP; + + // check the limits + camera.frustum.fov = Cesium.Math.clamp( + fov, + MIN_FOV, + MAX_FOV + ); + }, + { passive: false } + ); + + +} + +//Prevent the user to move in the scene +export function removeMoving(scene){ + scene.screenSpaceCameraController.enableRotate = false; +} +export async function loadPlanes(viewer, location){ + const airplaneUri = await Cesium.IonResource.fromAssetId(4359085); + const data = await getFLights('http://localhost:8080/flights', {long: location.long , lat: location.lat}) + + if(data !== undefined) { + const osmBuildings = await Cesium.createOsmBuildingsAsync(); + // Uncomment if you want 3d buildings dispalyed + // viewer.scene.primitives.add(osmBuildings); + + /* Initialize the viewer clock: + Assume the radar samples are 30 seconds apart, and calculate the entire flight duration based on that assumption. + Get the start and stop date times of the flight, where the start is the known flight departure time (converted from PST + to UTC) and the stop is the start plus the calculated duration. (Note that Cesium uses Julian dates. See + https://simple.wikipedia.org/wiki/Julian_day.) + Initialize the viewer's clock by setting its start and stop to the flight start and stop times we just calculated. + Also, set the viewer's current time to the start time and take the user to that time. + */ + // const totalSeconds = timeStepInSeconds * (flightData.length - 1); + const start = Cesium.JulianDate.now(); + const stop = Cesium.JulianDate.addSeconds(start, 86400, new Cesium.JulianDate()); + + viewer.clock.startTime = start.clone(); + viewer.clock.clockRange = Cesium.ClockRange.UNBOUNDED + viewer.clock.currentTime = start.clone(); + + // viewer.timeline.zoomTo(start, stop); + // Speed up the playback speed 50x. + viewer.clock.multiplier = 1; + // Start playing the scene. + viewer.clock.shouldAnimate = true; + + // The SampledPositionedProperty stores the position and timestamp for each sample along the radar sample series. + + for (let i = 0; i < data.length; i++) { + const positionProperty = new Cesium.SampledPositionProperty(); + + const flight = data[i]; + + // Declare the time for this individual sample and store it in a new JulianDate instance. + // const time = Cesium.JulianDate.addSeconds(start, i * timeStepInSeconds, new Cesium.JulianDate()); + + const position = Cesium.Cartesian3.fromDegrees(flight.long, flight.lat, flight.alt); + // Store the position along with its timestamp. + positionProperty.addSample(start, position); + // Make planes appear even if it's too late + positionProperty.forwardExtrapolationType = Cesium.ExtrapolationType.HOLD + positionProperty.backwardExtrapolationType = Cesium.ExtrapolationType.HOLD + + await loadModel(viewer, start, stop, positionProperty, airplaneUri, flight.id); + + positionProperty.addSample(getNextTimeBySecond(viewer, 60), determinatePlane(flight, 60)) + } + } + + +} + +async function loadModel(viewer, start, stop, positionProperty, airplaneUri, id) { + // Load the glTF model from Cesium ion. + viewer.entities.add({ + id: "plane_" + id, + availability: new Cesium.TimeIntervalCollection([ new Cesium.TimeInterval({ start: start, stop: stop }) ]), + position: positionProperty, + // Attach the 3D model instead of the green point. + model: {uri: airplaneUri,minimumPixelSize: 30 }, + // Automatically compute the orientation from the position. + orientation: new Cesium.VelocityOrientationProperty(positionProperty), + path: new Cesium.PathGraphics({ width: 3 , trailTime: 60}) + }); +} + +export async function updatePlanes(viewer, location){ + + const data = await getFLights('http://localhost:8080/flights', {long:location.long , lat: location.lat}) + if (data.length > 0){ + + if (viewer.entities !== undefined) { + for (const entity of viewer.entities.values) { + const flight = data.find(flight => entity.id.includes(flight.id)) + if(flight !== undefined && flight.id.includes('plane')){ + await addNextPostion(determinatePlane(flight, 60), entity, getNextTimeBySecond(viewer, 60)) + }else if(entity.id.includes('plane') ){ + viewer.entities.remove(entity) + } + } + } + + await addNewPlanes(viewer, data) + } + } + + +async function addNextPostion(nextPos, entity, futureTime){ + entity.position.addSample(futureTime, nextPos) +} + +function getNextTimeBySecond(viewer, seconds){ + return Cesium.JulianDate.addSeconds( + viewer.clock ? viewer.clock.currentTime : Cesium.JulianDate.now(), + seconds, + new Cesium.JulianDate() + ); +} +export async function sendToLogFile() { + /* + Source : https://brightdata.fr/blog/donnees-web/fetch-api-in-javascript + */ + + // Source : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from + // Convert map to array + let logsArray = Array.from(logs.values()); + + const url = "http://localhost:8080/logs" + + try { + const response = await fetch(url, { + method: "POST", + headers: { + 'Content-type': 'application/json; charset=UTF-8', + }, + // Source : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join + body: JSON.stringify({ + message: logsArray.join('\n'), + }), + }); + + // Clear the logs once they have been sent to the API + logs.clear(); + + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + } catch (error) { + console.error(error.message); + } +} + + + +function determinatePlane(flight, delta_time) { + /* + Prompt to Claude : + I want to predict the geographical position of an aircraft in 30 seconds. + + Available data: + - Current position: latitude (degrees), longitude (degrees), altitude (meters) + - Ground speed: m/s + - Heading: degrees (0° = North, 90° = East) + - Vertical speed: m/s + + Provide the complete mathematical formulas to calculate the new latitude, longitude, and altitude, + taking into account the curvature of the Earth. + */ + const long = flight.long + const lat = flight.lat + const alt = flight.alt + const speed = flight.velocity + const heading = flight.heading + const vertical_rate = flight.vertical_rate + + const pi = Math.PI; + + + // Convert to radians + const long_rad = long * pi / 180 + const lat_rad = lat * pi / 180 + const heading_rad = heading * pi / 180 + + const earth_radius = 6371000 // meters + + // Horizontal distance traveled + const distance = speed * delta_time + + // Calculate new latitude + const new_lat_rad = Math.asin( + Math.sin(lat_rad) * Math.cos(distance / earth_radius) + + Math.cos(lat_rad) * Math.sin(distance / earth_radius) * Math.cos(heading_rad) + ) + + // Calculate new longitude + const delta_long = Math.atan2( + Math.sin(heading_rad) * Math.sin(distance / earth_radius) * Math.cos(lat_rad), + Math.cos(distance / earth_radius) - Math.sin(lat_rad) * Math.sin(new_lat_rad) + ) + const new_long_rad = long_rad + delta_long + + // Calculate new altitude + const new_alt = alt + vertical_rate * delta_time + + // Convert result back to degrees + const new_lat = new_lat_rad * 180 / pi + const new_long = new_long_rad * 180 / pi + + + // Return predicted position + return Cesium.Cartesian3.fromDegrees(new_long, new_lat, new_alt); +} + +async function calculateMoonPlane(flight,viewer) { + /* + Prompt to Claude : + If I have a person P (with coordinates x, y, z), + a line segment representing the trajectory of an airplane from 0 to 60 seconds (points A_Now and A_Prediction), + and the moon L (x, y, z), + + I would like to know if the airplane passes in front of the moon from P's point of view at any point during those + 60 seconds + + Please provide the detailed mathematical formulas needed to calculate that. + */ + + const positionFlightNow = Cesium.Cartesian3.fromDegrees(flight.long, flight.lat, flight.alt); + + const x_A_Now = positionFlightNow.x; + const y_A_Now = positionFlightNow.y; + const z_A_Now = positionFlightNow.z; + + const positionFlightPrediction = determinatePlane(flight, 60) + + const x_A_Predict = positionFlightPrediction.x; + const y_A_Predict = positionFlightPrediction.y; + const z_A_Predict = positionFlightPrediction.z; + + const cameraPos = viewer.camera.position; + const x_P = cameraPos.x; + const y_P = cameraPos.y; + const z_P = cameraPos.z; + + + let moonPos = viewer.scene.moon.position; + if (!moonPos) { + moonPos = Cesium.Simon1994PlanetaryPositions.computeMoonPositionInEarthInertialFrame( + viewer.clock.currentTime + ); + } + + const x_L = moonPos.x; + const y_L = moonPos.y; + const z_L = moonPos.z; + + // P_To_L = P To Moon + // the direction is the moon + const P_To_L = { + x: x_L - x_P, + y: y_L - y_P, + z: z_L - z_P + }; + + // P_To_A_Now + // The direction of the plane now + const P_To_A_Now = { + x: x_A_Now - x_P, + y: y_A_Now - y_P, + z: z_A_Now - z_P + }; + + // the direction of the prediction + const A_Now_To_A_Pred = { + x: x_A_Predict - x_A_Now, + y: y_A_Predict - y_A_Now, + z: z_A_Predict - z_A_Now + }; + + // normalize moon direction + // distance between the observer and the moon + const norm_P_To_L = Math.sqrt( + P_To_L.x ** 2 + + P_To_L.y ** 2 + + P_To_L.z ** 2 + ) + + // keep only the direction + const u_L = { + x: P_To_L.x / norm_P_To_L, + y: P_To_L.y / norm_P_To_L, + z: P_To_L.z / norm_P_To_L + }; + + // Dot product: P_To_A_Now · u_L + // how far the plane is already pointing towards the moon + const P_To_A_Now_dot_u_L = + P_To_A_Now.x * u_L.x + + P_To_A_Now.y * u_L.y + + P_To_A_Now.z * u_L.z; + + // Dot product: A_Now_To_A_Pred · u_L + // indicates whether the aircraft is moving towards or away from the lunar direction. + const A_Now_To_A_Pred_dot_u_L = + A_Now_To_A_Pred.x * u_L.x + + A_Now_To_A_Pred.y * u_L.y + + A_Now_To_A_Pred.z * u_L.z; + + // time the plane is closest to the moon + let t_closest; + + // handle cases where the result is almost zero + const epsilon = 1e-10; + + if (Math.abs(A_Now_To_A_Pred_dot_u_L) < epsilon) { + // Trajectory perpendicular to moon direction + t_closest = 0; + + } else { + // calculate the moment when the plane will be closest to the moon. + const t_star = -60 * P_To_A_Now_dot_u_L / A_Now_To_A_Pred_dot_u_L; + // limit the result between 0 and 60 seconds + t_closest = Math.max(0, Math.min(60, t_star)); + } + + + // Converts time into a ratio between 0 and 1. + const t_ratio = t_closest / 60; + + // calculates the position of the plane at the moment when the plane is closest to the moon + const P_To_A_At_t_closest = { + x: P_To_A_Now.x + t_ratio * A_Now_To_A_Pred.x, + y: P_To_A_Now.y + t_ratio * A_Now_To_A_Pred.y, + z: P_To_A_Now.z + t_ratio * A_Now_To_A_Pred.z + }; + + // calculate the length of the observer vector + const norm_P_To_A_At_t_closest = Math.sqrt( + P_To_A_At_t_closest.x ** 2 + + P_To_A_At_t_closest.y ** 2 + + P_To_A_At_t_closest.z ** 2 + ); + + // in order to calculate the angle between the two directions + const dot_product = + P_To_A_At_t_closest.x * P_To_L.x + + P_To_A_At_t_closest.y * P_To_L.y + + P_To_A_At_t_closest.z * P_To_L.z; + + // calculate the cosine of the angle between the two directions + /* + cos(0°) = 1 → same directions + cos(90°) = 0 → perpendicular directions + cos(180°) = -1 → opposite directions + */ + const cos_theta = dot_product / (norm_P_To_A_At_t_closest * norm_P_To_L); + + + // we force the value into the valid range + const cos_theta_clamped = Math.max(-1, Math.min(1, cos_theta)); + + // Calculate the angle in radians between the direction of the plane and the direction of the moon. + const corner_O = Math.acos(cos_theta_clamped); + + const moonAngularRadius = 0.0045 + const closeTheMoon = (0.0135)*2 + + // Source : https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Map/has + const key = `${flight.id}` + + const currentLog = logs.get(flight.id); + const user_email = getEmail() + + if (corner_O <= moonAngularRadius) { + if (!currentLog) { + const closestDate = new Date(Date.now() + t_closest * 1000); + const logTime = closestDate.toISOString(); + logs.set(key, `Aircraft pass in front of the moon | Camera : ${cameraPos} | Aircraft ID : ${flight.id} + | Time : ${logTime}`); + + if(user_email !== undefined){ + await sendEmail(user_email, ` ${logTime} : Aircraft pass in front of the moon | Camera : ${cameraPos} | Aircraft ID : ${flight.id} `) + } + } + } else if (corner_O <= closeTheMoon) { + // Source : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith + if (!currentLog || !currentLog.startsWith("Aircraft pass in front")) { + const closestDate = new Date(Date.now() + t_closest * 1000); + const logTime = closestDate.toISOString(); + logs.set(key, `Aircraft pass close to the moon | Camera : ${cameraPos} | Aircraft ID : ${flight.id} + | Time : ${logTime}`); + if(user_email !== undefined){ + await sendEmail(user_email, ` ${logTime} : Aircraft pass close of the moon | Camera : ${cameraPos} | Aircraft ID : ${flight.id} `) + } + } + } +} + +async function addNewPlanes(viewer, data){ + const airplaneUri = await Cesium.IonResource.fromAssetId(4359085); + for (const flight of data) { + if (viewer.entities.values.find(entity => entity.id.includes(flight.id)) === undefined){ + const positionProperty = new Cesium.SampledPositionProperty(); + const position = Cesium.Cartesian3.fromDegrees(flight.long, flight.lat, flight.alt) + positionProperty.addSample(viewer.clock.currentTime, position); + positionProperty.forwardExtrapolationType = Cesium.ExtrapolationType.HOLD + positionProperty.backwardExtrapolationType = Cesium.ExtrapolationType.HOLD + await loadModel(viewer, viewer.clock.currentTime, viewer.clock.stopTime, positionProperty, airplaneUri, flight.id) + positionProperty.addSample(getNextTimeBySecond(viewer, 60), determinatePlane(flight, 60)) + } + } +} + +export async function checkIfPlaneIsCloseToTheMoon(viewer, location) { + const data = await getFLights('http://localhost:8080/flights', {long:location.long , lat: location.lat}) + data.forEach(flight => { + calculateMoonPlane(flight, viewer) + }) +} + +// Source : https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval diff --git a/src/utils/tileset.js b/src/utils/tileset.js new file mode 100644 index 0000000..178363b --- /dev/null +++ b/src/utils/tileset.js @@ -0,0 +1,7 @@ +export async function get3dTilesetById(id) { + return await Cesium.Cesium3DTileset.fromIonAssetId(id) +} + +export async function getTilesetCartesian(){ + return {x:-24,y: 12, z:45} +}