From 0f79cefdac3caccb580118f87dc7765fe23227fa Mon Sep 17 00:00:00 2001 From: ThePetrovich Date: Wed, 2 Jul 2025 15:32:46 +0800 Subject: [PATCH] Implement basic saved point editor --- package-lock.json | 10 ++ package.json | 1 + src/lib/api/base.ts | 64 ++++++++ src/lib/api/points.ts | 19 +++ src/lib/components/ControlPanel.svelte | 115 ++++++++++---- src/lib/components/PointEditor.svelte | 8 + src/lib/components/PointListModal.svelte | 181 +++++++++++++++++++++++ src/lib/prediction.ts | 10 +- src/lib/stores.ts | 3 + src/lib/types.ts | 11 +- src/routes/predict/+page.svelte | 24 ++- static/css/custom.css | 9 ++ 12 files changed, 414 insertions(+), 41 deletions(-) create mode 100644 src/lib/api/base.ts create mode 100644 src/lib/api/points.ts create mode 100644 src/lib/components/PointEditor.svelte create mode 100644 src/lib/components/PointListModal.svelte diff --git a/package-lock.json b/package-lock.json index 844ec4e..c48f9a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@vincjo/datatables": "^2.5.0", "svelte": "^5.34.8", "svelte-check": "^4.0.0", "typescript": "^5.0.0", @@ -881,6 +882,15 @@ "@types/geojson": "*" } }, + "node_modules/@vincjo/datatables": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@vincjo/datatables/-/datatables-2.5.0.tgz", + "integrity": "sha512-DvlgTmjRFnzIQwIx883+B+66OFnHriMLLh9493QiduWyNtidhYADyyVwlrtcCRH4p+oYL4L9qM1sTLlARzNMxA==", + "dev": true, + "peerDependencies": { + "svelte": "^5.16.0" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", diff --git a/package.json b/package.json index 9d82df6..905bfba 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@vincjo/datatables": "^2.5.0", "svelte": "^5.34.8", "svelte-check": "^4.0.0", "typescript": "^5.0.0", diff --git a/src/lib/api/base.ts b/src/lib/api/base.ts new file mode 100644 index 0000000..c25771d --- /dev/null +++ b/src/lib/api/base.ts @@ -0,0 +1,64 @@ +import { getCsrfToken } from "$lib/auth"; + +export const API_BASE_URL = "http://localhost:8000/api"; + +export async function fetchAPI(endpoint: string, options: RequestInit = {}): Promise { + let csrfToken = await getCsrfToken(); + if (!csrfToken) { + console.warn("CSRF token not found, using empty string."); + csrfToken = ""; + } + const url = `${API_BASE_URL}${endpoint}`; + options.credentials = "include"; // Include cookies in the request + options.headers = { + ...options.headers, + "Content-Type": "application/json", + "X-CSRFToken": csrfToken, + }; + try { + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + if (response.status === 204) { + // No content response + return {} as T; // Return an empty object for 204 responses + } + return await response.json() as T; + } catch (error) { + console.error(`Error fetching ${url}:`, error); + throw error; + } +} + +export function postAPI(endpoint: string, data: any): Promise { + return fetchAPI(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); +} + +export function getAPI(endpoint: string): Promise { + return fetchAPI(endpoint, { + method: "GET", + }); +} + +export function putAPI(endpoint: string, data: any): Promise { + return fetchAPI(endpoint, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); +} + +export function deleteAPI(endpoint: string): Promise { + return fetchAPI(endpoint, { + method: "DELETE", + }); +} \ No newline at end of file diff --git a/src/lib/api/points.ts b/src/lib/api/points.ts new file mode 100644 index 0000000..4f655a2 --- /dev/null +++ b/src/lib/api/points.ts @@ -0,0 +1,19 @@ +/* API functions for Saved Points */ +import type { SavedPoint } from "$lib/types"; +import { getAPI, postAPI, putAPI, deleteAPI } from "./base"; + +export function getSavedPoints(): Promise { + return getAPI("/saved-points/"); +} + +export function savePoint(point: SavedPoint): Promise { + return postAPI("/saved-points/", point); +} + +export function updatePoint(point: SavedPoint): Promise { + return putAPI(`/saved-points/${point.id}/`, point); +} + +export function deletePoint(id: number): Promise { + return deleteAPI(`/saved-points/${id}/`); +} \ No newline at end of file diff --git a/src/lib/components/ControlPanel.svelte b/src/lib/components/ControlPanel.svelte index cc0449d..c1bd305 100644 --- a/src/lib/components/ControlPanel.svelte +++ b/src/lib/components/ControlPanel.svelte @@ -15,7 +15,10 @@ import { getForecast } from "$lib/prediction"; import type { FlightParameters, ProfileName } from "$lib/types"; import { PROFILE_MAP } from "$lib/types"; - import { FlightParametersStore, writeLocalStorage } from "$lib/stores"; + import { SavedPointsStore, FlightParametersStore, writeLocalStorage } from "$lib/stores"; + import { getSavedPoints } from "$lib/api/points"; + import type { SavedPoint } from "$lib/types"; + import { onMount } from "svelte"; let isCollapsed = false; let selectedProfile: ProfileName = "Normal"; @@ -27,14 +30,55 @@ let startDate = now.toISOString().split("T")[0]; // YYYY-MM-DD let startTime = now.toISOString().split("T")[1].split(".")[0]; // HH:MM:SS - let inputLat = $FlightParametersStore.launch_latitude.toString(); - let inputLng = $FlightParametersStore.launch_longitude.toString(); + let inputLat = $FlightParametersStore.launch_latitude.toFixed(6).toString(); + let inputLng = $FlightParametersStore.launch_longitude.toFixed(6).toString(); $: $FlightParametersStore = { ...$FlightParametersStore, profile: PROFILE_MAP[selectedProfile], }; + $: $SavedPointsStore, setCoordinatesFromSavedPoint(); + + $: inputLat, inputLng, setToCustomOnChange(); + + function setCoordinatesFromSavedPoint() { + console.log("Start point changed:", startPoint); + + if (startPoint === "Custom") { + $FlightParametersStore.launch_latitude = parseFloat(inputLat); + $FlightParametersStore.launch_longitude = parseFloat(inputLng); + } else { + const selectedOption = document.querySelector( + `#startPoint option[value="${startPoint}"]` + ) as HTMLOptionElement; + if (selectedOption) { + const lat = parseFloat(selectedOption.getAttribute("data-lat") || "0"); + const lng = parseFloat(selectedOption.getAttribute("data-lng") || "0"); + const alt = parseFloat(selectedOption.getAttribute("data-alt") || "0"); + inputLat = lat.toFixed(6).toString(); + inputLng = lng.toFixed(6).toString(); + $FlightParametersStore.launch_latitude = lat; + $FlightParametersStore.launch_longitude = lng; + $FlightParametersStore.launch_altitude = alt; + console.log("Updated position from saved point:", lat, lng, alt); + } + } + } + + function setToCustomOnChange() { + if (startPoint !== "Custom") { + startPoint = "Custom"; + console.log("Switched to Custom point"); + } + } + + onMount(async () => { + // Load saved points from the server or local storage + const savedPoints = await getSavedPoints(); + SavedPointsStore.set(savedPoints); + }); + const handleGetPrediction = async () => { console.log("Fetching prediction with parameters:", $FlightParametersStore); console.log(startDate, startTime); @@ -58,6 +102,10 @@ console.log("Select on map clicked"); } + export let handleClickPointListModal = () => { + console.log("Open Point List Modal"); + }; + const applyCoordinatesFromInput = () => { const lat = parseFloat(inputLat); const lng = parseFloat(inputLng); @@ -179,15 +227,22 @@ - - - - + + + {#each $SavedPointsStore as point} + + {/each} + + + + diff --git a/src/lib/components/PointEditor.svelte b/src/lib/components/PointEditor.svelte new file mode 100644 index 0000000..02cfb5b --- /dev/null +++ b/src/lib/components/PointEditor.svelte @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/src/lib/components/PointListModal.svelte b/src/lib/components/PointListModal.svelte new file mode 100644 index 0000000..34eaa30 --- /dev/null +++ b/src/lib/components/PointListModal.svelte @@ -0,0 +1,181 @@ + + + + + + + + diff --git a/src/lib/prediction.ts b/src/lib/prediction.ts index 2eed534..1e66d65 100644 --- a/src/lib/prediction.ts +++ b/src/lib/prediction.ts @@ -3,7 +3,7 @@ import type { LatLngExpression } from "leaflet"; import L from "leaflet"; import { getCsrfToken } from "./auth"; -import type { PredictionStage, RawPrediction, Prediction } from "./types"; +import type { PredictionStage, RawPrediction, Prediction, Point } from "./types"; import { PredictionStore, RawPredictionStore, writeLocalStorage } from "./stores"; function getLatestDataset() { @@ -90,10 +90,10 @@ export const getForecast = async ( }; export function parsePrediction(prediction: PredictionStage[]): Prediction { - 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 flight_path: LatLngExpression[] = []; + const launch: Point = {} as any; + const burst: Point = {} as any; + const landing: Point = {} as any; const ascent = prediction[0].trajectory; const descent = prediction[1].trajectory; diff --git a/src/lib/stores.ts b/src/lib/stores.ts index 03a3c2c..2f2c50b 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -1,6 +1,7 @@ import { writable } from "svelte/store"; import type { FlightParameters, RawTelemetry, Telemetry } from "./types"; import type { RawPrediction, Prediction } from "./types"; +import type { SavedPoint } from "./types"; export const readLocalStorage = (key: string, defaultValue: T): T => { const item = localStorage.getItem(key); @@ -67,3 +68,5 @@ export const RawPredictionStore = writable( export const PredictionStore = writable( readLocalStorage("prediction", {} as Prediction) ); + +export const SavedPointsStore = writable([]); diff --git a/src/lib/types.ts b/src/lib/types.ts index 388f8d0..27c54a3 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -21,10 +21,11 @@ export interface FlightParameters { launch_longitude: number; profile: (typeof PROFILE_MAP)[ProfileName]; version: number; + start_point?: string; // Optional, used for saved points } export interface Point { - latlng: LatLngLiteral & { alt: number }; + latlng: LatLngLiteral & { alt?: number }; datetime: Date; } @@ -83,3 +84,11 @@ export interface Prediction { profile: string; flight_time: number; } + +export interface SavedPoint { + id: number; + name: string; + lat: number; + lon: number; + alt: number; +} \ No newline at end of file diff --git a/src/routes/predict/+page.svelte b/src/routes/predict/+page.svelte index 62fc819..7a276a0 100644 --- a/src/routes/predict/+page.svelte +++ b/src/routes/predict/+page.svelte @@ -3,15 +3,14 @@ import ControlPanel from "$lib/components/ControlPanel.svelte"; import Navbar from "$lib/components/Navbar.svelte"; import PanelContainer from "$lib/components/PanelContainer.svelte"; - import TelemetryPanel from '$lib/components/TelemetryPanel.svelte'; import ScenarioPanel from "$lib/components/ScenarioPanel.svelte"; import TabComponent from "$lib/components/TabComponent.svelte"; + import PointListModal from "$lib/components/PointListModal.svelte"; import { onMount } from "svelte"; import { PredictionStore } from "$lib/stores"; - import { Modal, Icon } from "@sveltestrap/sveltestrap"; - import Toast, { addToast, removeToast } from "$lib/components/Toast.svelte"; + import { addToast, removeToast } from "$lib/components/Toast.svelte"; import ToastContainer from '$lib/components/Toast.svelte'; - import L from "leaflet"; + import L, { point } from "leaflet"; let map: Map | null = null; let panelContainer: PanelContainer | null = null; @@ -19,6 +18,8 @@ let selectionToastId: string | null = null; let activeTab: 'control' | 'scenario' | 'settings' | 'about' = 'scenario'; + let pointListModal: PointListModal | null = null; + onMount(() => { PredictionStore.subscribe((data) => { if (data) { @@ -65,6 +66,16 @@ selectionToastId = null; } } + + function handleClickPointListModal() { + if (map) { + map.stopSelection(); + console.log("Selection mode disabled"); + } + pointListModal?.openModal(); + } + +
@@ -73,7 +84,7 @@ {#if activeTab === 'control'} - + {:else if activeTab === 'scenario'} {:else if activeTab === 'settings'} @@ -94,5 +105,6 @@ +
diff --git a/static/css/custom.css b/static/css/custom.css index e716523..c118b4c 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -113,6 +113,15 @@ box-shadow: none !important; } +.modal-backdrop { + opacity: var(--bs-backdrop-opacity) !important; +} + +.table td.fit, +.table th.fit { + white-space: nowrap; + width: 1%; +} @media (max-width: 767.98px) {