From 55295b84aadc2348bc62e848ce77d90de6a4d9ae Mon Sep 17 00:00:00 2001 From: ThePetrovich Date: Sat, 5 Apr 2025 14:43:23 +0800 Subject: [PATCH] Add initial plotting --- jsconfig.json | 1 + src/lib/mathutil.ts | 24 ++++ src/lib/prediction.ts | 198 ++++++++++++++++++++++++++++++ src/routes/BurstCalculator.svelte | 194 ----------------------------- src/routes/ControlPanel.svelte | 183 ++++++++++----------------- src/routes/leaflet.svelte | 120 ------------------ src/routes/map.svelte | 78 +++++++++++- static/pop-marker.png | Bin 0 -> 776 bytes static/target-blue.png | Bin 0 -> 3346 bytes static/target-red.png | Bin 0 -> 3255 bytes 10 files changed, 362 insertions(+), 436 deletions(-) create mode 100644 src/lib/mathutil.ts create mode 100644 src/lib/prediction.ts delete mode 100644 src/routes/BurstCalculator.svelte delete mode 100644 src/routes/leaflet.svelte create mode 100644 static/pop-marker.png create mode 100644 static/target-blue.png create mode 100644 static/target-red.png diff --git a/jsconfig.json b/jsconfig.json index 0b2d886..c4103ce 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "allowJs": true, "checkJs": true, + "allowImportingTsExtensions": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, diff --git a/src/lib/mathutil.ts b/src/lib/mathutil.ts new file mode 100644 index 0000000..75bcebe --- /dev/null +++ b/src/lib/mathutil.ts @@ -0,0 +1,24 @@ +export function distHaversine( + p1: { lat: number; lng: number }, + p2: { lat: number; lng: number }, + precision?: number +): string { + const R = 6371; // Earth's mean radius in km + + const rad = (x: number): number => (x * Math.PI) / 180; + + const dLat = rad(p2.lat - p1.lat); + const dLong = rad(p2.lng - p1.lng); + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(rad(p1.lat)) * + Math.cos(rad(p2.lat)) * + Math.sin(dLong / 2) * + Math.sin(dLong / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const d = R * c; + + return d.toFixed(precision ?? 3); +} \ No newline at end of file diff --git a/src/lib/prediction.ts b/src/lib/prediction.ts new file mode 100644 index 0000000..a5e60c6 --- /dev/null +++ b/src/lib/prediction.ts @@ -0,0 +1,198 @@ +import { writable } from "svelte/store" +import type { LatLngExpression } from "leaflet"; +import L from "leaflet"; + +interface TrajectoryPoint { + altitude: number; + datetime: string; + latitude: number; + longitude: number; +} + +interface PredictionStage { + stage: string; + trajectory: TrajectoryPoint[]; +} + +interface ParsedPrediction { + flight_path: [number, number, number][]; + launch: { + latlng: LatLngExpression; + datetime: Date; + }; + burst: { + latlng: LatLngExpression; + datetime: Date; + }; + landing: { + latlng: LatLngExpression; + datetime: Date; + }; + profile: string; + flight_time: number; +} + + +export const latestPrediction = writable({ + metadata: { + complete_datetime: "", + start_datetime: "" + }, + prediction: [ + { + stage: "", + trajectory: [ + { + altitude: 0.0, + datetime: "", + latitude: 0.0, + longitude: 0.0 + } + ] + } + ] +}); + +export const latestPredictionParsed = writable({} as ParsedPrediction); + +function getLatestDataset() { + const now = new Date(); + const hours = now.getUTCHours(); + const minutes = now.getUTCMinutes(); + const seconds = now.getUTCSeconds(); + + // Round down to the nearest 6-hour interval + const roundedHours = Math.floor(hours / 6) * 6; + const roundedDate = new Date(now); + roundedDate.setUTCHours(roundedHours, 0, 0, 0); + + // Subtract 6 hours to account for the lag + roundedDate.setUTCHours(roundedDate.getUTCHours() - 6); + + return roundedDate.toISOString(); +} + +function formatLaunchDateTime(dateObj: string | Date, timeStr: string): string { + // Ensure date is a Date object + const date = new Date(dateObj); + + // Extract date components + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + // Format time (ensure it has seconds) + let formattedTime = timeStr; + if (timeStr.split(':').length === 2) { + formattedTime += ':00'; // Add seconds if missing + } + + // Combine into ISO string + const isoString = new Date(`${year}-${month}-${day}T${formattedTime}Z`).toISOString(); + + return isoString; +} + +export const getForecast = async (flightParameters: Record, startDate: string, startTime: string): Promise => { + const launch_datetime = formatLaunchDateTime(startDate, startTime); + + // Create request object + flightParameters.dataset = getLatestDataset(); + flightParameters.launch_datetime = launch_datetime; + + console.log("Sending request:", flightParameters); + + try { + // Example POST request - replace with your actual API endpoint + const response = await fetch('http://127.0.0.1:8000/api/predictions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(flightParameters) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log("Forecast response:", data); + + latestPrediction.set(data.result); + latestPredictionParsed.set(parsePrediction(data.result.prediction)); + + alert("Forecast request successful!"); + // Handle the response data as needed + } catch (error) { + console.error("Error sending forecast request:", error); + alert("Error getting forecast: " + error); + } +}; + +export function parsePrediction(prediction: PredictionStage[]): ParsedPrediction { + const flight_path: [number, number, number][] = []; + const launch: { latlng: LatLngExpression; datetime: Date } = {} as any; + const burst: { latlng: LatLngExpression; datetime: Date } = {} as any; + const landing: { latlng: LatLngExpression; datetime: Date } = {} as any; + + const ascent = prediction[0].trajectory; + const descent = prediction[1].trajectory; + + // Add the ascent track to the flight path array. + ascent.forEach((item) => { + let lon = item.longitude; + if (lon > 180.0) { + lon -= 360.0; + } + + flight_path.push([item.latitude, lon, item.altitude]); + }); + + // Add the descent track to the flight path array. + descent.forEach((item) => { + let lon = item.longitude; + if (lon > 180.0) { + lon -= 360.0; + } + + flight_path.push([item.latitude, lon, item.altitude]); + }); + + // Populate the launch, burst, and landing points + const launchObj = ascent[0]; + let lon = launchObj.longitude; + if (lon > 180.0) { + lon -= 360.0; + } + launch.latlng = L.latLng([launchObj.latitude, lon, launchObj.altitude]); + launch.datetime = new Date(launchObj.datetime); + + const burstObj = descent[0]; + lon = burstObj.longitude; + if (lon > 180.0) { + lon -= 360.0; + } + burst.latlng = L.latLng([burstObj.latitude, lon, burstObj.altitude]); + burst.datetime = new Date(burstObj.datetime); + + const landingObj = descent[descent.length - 1]; + lon = landingObj.longitude; + if (lon > 180.0) { + lon -= 360.0; + } + landing.latlng = L.latLng([landingObj.latitude, lon, landingObj.altitude]); + landing.datetime = new Date(landingObj.datetime); + + const profile = prediction[1].stage === "descent" ? "standard_profile" : "float_profile"; + const flight_time = (new Date(landing.datetime).getTime() - new Date(launch.datetime).getTime()) / 1000; + + return { + flight_path, + launch, + burst, + landing, + profile, + flight_time, + }; +} diff --git a/src/routes/BurstCalculator.svelte b/src/routes/BurstCalculator.svelte deleted file mode 100644 index 48a68b8..0000000 --- a/src/routes/BurstCalculator.svelte +++ /dev/null @@ -1,194 +0,0 @@ - \ No newline at end of file diff --git a/src/routes/ControlPanel.svelte b/src/routes/ControlPanel.svelte index 3f9c82d..b4adb66 100644 --- a/src/routes/ControlPanel.svelte +++ b/src/routes/ControlPanel.svelte @@ -1,114 +1,57 @@ -
+
-
Prediction Parameters
+
Параметры прогнозирования
- +
- + / - +
+ get_map_position(); + }}>Указать на карте
- - + +
- +
- +
- - + +
- - + +
- - + +
- +
- flightParameters.profile = profileMap[profile] || 'standard_profile'}> -
- - - + + +
{/if} diff --git a/src/routes/leaflet.svelte b/src/routes/leaflet.svelte deleted file mode 100644 index 014cb60..0000000 --- a/src/routes/leaflet.svelte +++ /dev/null @@ -1,120 +0,0 @@ - - -
-
-
- Lat: {mouseLat}, Long: {mouseLng} -
-
- diff --git a/src/routes/map.svelte b/src/routes/map.svelte index 270704f..840212c 100644 --- a/src/routes/map.svelte +++ b/src/routes/map.svelte @@ -2,9 +2,12 @@ import { onMount } from 'svelte'; import * as L from 'leaflet'; import 'leaflet/dist/leaflet.css'; + import { distHaversine } from '../lib/mathutil.ts'; + + import { latestPredictionParsed } from '../lib/prediction.ts'; /** - * @type {{ removeLayer: (arg0: any) => void; setView: (arg0: number[], arg1: any) => void; getZoom: () => any; on: (arg0: string, arg1: (e: any) => void) => void; }} + * @type {L.Map} */ let map; let mouseLat = 0; @@ -70,7 +73,80 @@ Launch Point
, Lat: ${marker.getLatLng().lat.toFixed(6)}
, Long: ${marker.getLatLng().lng.toFixed(6)}
`; }); + + latestPredictionParsed.subscribe((prediction) => { + if (prediction) { + plotPrediction(prediction); + } + }); }); + + const plotPrediction = (prediction) => { + console.log("Flight data parsed, creating map plot..."); + + // Clear existing map items + if (marker) { + map.eachLayer((layer) => { + if (layer instanceof L.Marker || layer instanceof L.Polyline) { + map.removeLayer(layer); + } + }); + } + + const { launch, landing, burst, flight_path, flight_time } = prediction; + + // Calculate range and time of flight + const range = distHaversine(launch.latlng, landing.latlng, 1); + const f_hours = Math.floor(flight_time / 3600); + const f_minutes = Math.floor(((flight_time % 86400) % 3600) / 60).toString().padStart(2, '0'); + const flighttime = `${f_hours}hr${f_minutes}`; + console.log(`Range: ${range}, Flight Time: ${flighttime}`); + + // Create custom icons + const launchIcon = L.icon({ + iconUrl: 'target-blue.png', + iconSize: [10, 10], + iconAnchor: [5, 5], + }); + + const landIcon = L.icon({ + iconUrl: 'target-red.png', + iconSize: [10, 10], + iconAnchor: [5, 5], + }); + + const burstIcon = L.icon({ + iconUrl: 'pop-marker.png', + iconSize: [16, 16], + iconAnchor: [8, 8], + }); + + // Add markers to the map + const launchMarker = L.marker(launch.latlng, { + title: `Launch (${launch.latlng.lat.toFixed(4)}, ${launch.latlng.lng.toFixed(4)}) at ${launch.datetime.toUTCString()}`, + icon: launchIcon, + }).addTo(map); + + const landMarker = L.marker(landing.latlng, { + title: `Landing (${landing.latlng.lat.toFixed(4)}, ${landing.latlng.lng.toFixed(4)}) at ${landing.datetime.toUTCString()}`, + icon: landIcon, + }).addTo(map); + + const burstMarker = L.marker(burst.latlng, { + title: `Burst (${burst.latlng.lat.toFixed(4)}, ${burst.latlng.lng.toFixed(4)} at altitude ${burst.latlng.alt.toFixed(0)}) at ${burst.datetime.toUTCString()}`, + icon: burstIcon, + }).addTo(map); + + // Add flight path polyline + const pathPolyline = L.polyline(flight_path, { + weight: 3, + color: "#000000", + }).addTo(map); + + // Center the map on the launch point + map.setView(launch.latlng, 8); + }; +
diff --git a/static/pop-marker.png b/static/pop-marker.png new file mode 100644 index 0000000000000000000000000000000000000000..a111bfe7a3be3ba5877ed36258e1bd63b66b8ea5 GIT binary patch literal 776 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 literal 0 HcmV?d00001 diff --git a/static/target-red.png b/static/target-red.png new file mode 100644 index 0000000000000000000000000000000000000000..c3222b860ac024fea44fe459ea1fb315506aba24 GIT binary patch literal 3255 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