Implement basic saved point editor
This commit is contained in:
parent
bb390d50dc
commit
0f79cefdac
12 changed files with 414 additions and 41 deletions
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -20,6 +20,7 @@
|
||||||
"@sveltejs/adapter-auto": "^4.0.0",
|
"@sveltejs/adapter-auto": "^4.0.0",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@vincjo/datatables": "^2.5.0",
|
||||||
"svelte": "^5.34.8",
|
"svelte": "^5.34.8",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
|
|
@ -881,6 +882,15 @@
|
||||||
"@types/geojson": "*"
|
"@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": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@sveltejs/adapter-auto": "^4.0.0",
|
"@sveltejs/adapter-auto": "^4.0.0",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@vincjo/datatables": "^2.5.0",
|
||||||
"svelte": "^5.34.8",
|
"svelte": "^5.34.8",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
|
|
|
||||||
64
src/lib/api/base.ts
Normal file
64
src/lib/api/base.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { getCsrfToken } from "$lib/auth";
|
||||||
|
|
||||||
|
export const API_BASE_URL = "http://localhost:8000/api";
|
||||||
|
|
||||||
|
export async function fetchAPI<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
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<T>(endpoint: string, data: any): Promise<T> {
|
||||||
|
return fetchAPI<T>(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAPI<T>(endpoint: string): Promise<T> {
|
||||||
|
return fetchAPI<T>(endpoint, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putAPI<T>(endpoint: string, data: any): Promise<T> {
|
||||||
|
return fetchAPI<T>(endpoint, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAPI<T>(endpoint: string): Promise<T> {
|
||||||
|
return fetchAPI<T>(endpoint, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
}
|
||||||
19
src/lib/api/points.ts
Normal file
19
src/lib/api/points.ts
Normal file
|
|
@ -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<SavedPoint[]> {
|
||||||
|
return getAPI<SavedPoint[]>("/saved-points/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePoint(point: SavedPoint): Promise<SavedPoint> {
|
||||||
|
return postAPI<SavedPoint>("/saved-points/", point);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePoint(point: SavedPoint): Promise<SavedPoint> {
|
||||||
|
return putAPI<SavedPoint>(`/saved-points/${point.id}/`, point);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePoint(id: number): Promise<void> {
|
||||||
|
return deleteAPI<void>(`/saved-points/${id}/`);
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,10 @@
|
||||||
import { getForecast } from "$lib/prediction";
|
import { getForecast } from "$lib/prediction";
|
||||||
import type { FlightParameters, ProfileName } from "$lib/types";
|
import type { FlightParameters, ProfileName } from "$lib/types";
|
||||||
import { PROFILE_MAP } 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 isCollapsed = false;
|
||||||
let selectedProfile: ProfileName = "Normal";
|
let selectedProfile: ProfileName = "Normal";
|
||||||
|
|
@ -27,14 +30,55 @@
|
||||||
let startDate = now.toISOString().split("T")[0]; // YYYY-MM-DD
|
let startDate = now.toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
let startTime = now.toISOString().split("T")[1].split(".")[0]; // HH:MM:SS
|
let startTime = now.toISOString().split("T")[1].split(".")[0]; // HH:MM:SS
|
||||||
|
|
||||||
let inputLat = $FlightParametersStore.launch_latitude.toString();
|
let inputLat = $FlightParametersStore.launch_latitude.toFixed(6).toString();
|
||||||
let inputLng = $FlightParametersStore.launch_longitude.toString();
|
let inputLng = $FlightParametersStore.launch_longitude.toFixed(6).toString();
|
||||||
|
|
||||||
$: $FlightParametersStore = {
|
$: $FlightParametersStore = {
|
||||||
...$FlightParametersStore,
|
...$FlightParametersStore,
|
||||||
profile: PROFILE_MAP[selectedProfile],
|
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 () => {
|
const handleGetPrediction = async () => {
|
||||||
console.log("Fetching prediction with parameters:", $FlightParametersStore);
|
console.log("Fetching prediction with parameters:", $FlightParametersStore);
|
||||||
console.log(startDate, startTime);
|
console.log(startDate, startTime);
|
||||||
|
|
@ -58,6 +102,10 @@
|
||||||
console.log("Select on map clicked");
|
console.log("Select on map clicked");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let handleClickPointListModal = () => {
|
||||||
|
console.log("Open Point List Modal");
|
||||||
|
};
|
||||||
|
|
||||||
const applyCoordinatesFromInput = () => {
|
const applyCoordinatesFromInput = () => {
|
||||||
const lat = parseFloat(inputLat);
|
const lat = parseFloat(inputLat);
|
||||||
const lng = parseFloat(inputLng);
|
const lng = parseFloat(inputLng);
|
||||||
|
|
@ -179,15 +227,22 @@
|
||||||
<FormGroup spacing="mb-2">
|
<FormGroup spacing="mb-2">
|
||||||
<Label for="startPoint" class="form-label">Точка старта:</Label>
|
<Label for="startPoint" class="form-label">Точка старта:</Label>
|
||||||
<InputGroup size="sm">
|
<InputGroup size="sm">
|
||||||
<Input type="select" id="startPoint" bind:value={startPoint}>
|
<Input type="select" id="startPoint" bind:value={startPoint} on:change={setCoordinatesFromSavedPoint}>
|
||||||
<option>Custom</option>
|
<optgroup label="Сохраненные точки">
|
||||||
<option>Preset 1</option>
|
{#each $SavedPointsStore as point}
|
||||||
<option>Preset 2</option>
|
<option value={point.name} data-lat={point.lat} data-lng={point.lon} data-alt={point.alt}>
|
||||||
|
{point.name}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Задать вручную">
|
||||||
|
<option value="Custom">Custom</option>
|
||||||
|
</optgroup>
|
||||||
</Input>
|
</Input>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
title="Edit Saved Locations"
|
title="Edit Saved Locations"
|
||||||
on:click={() => console.log("Not implemented yet")}
|
on:click={handleClickPointListModal}
|
||||||
>
|
>
|
||||||
<span>Редакт.</span>
|
<span>Редакт.</span>
|
||||||
<Icon name="journal-bookmark-fill" />
|
<Icon name="journal-bookmark-fill" />
|
||||||
|
|
@ -236,27 +291,29 @@
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2 d-flex gap-2">
|
{#if selectedProfile != "Custom"}
|
||||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
<div class="mb-2 d-flex gap-2">
|
||||||
<Label for="ascentRate" class="form-label">Скорость подъема (м/с):</Label>
|
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||||
<Input
|
<Label for="ascentRate" class="form-label">Скорость подъема (м/с):</Label>
|
||||||
type="number"
|
<Input
|
||||||
id="ascentRate"
|
type="number"
|
||||||
class="form-control-sm"
|
id="ascentRate"
|
||||||
bind:value={$FlightParametersStore.ascent_rate}
|
class="form-control-sm"
|
||||||
/>
|
bind:value={$FlightParametersStore.ascent_rate}
|
||||||
</FormGroup>
|
/>
|
||||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
</FormGroup>
|
||||||
<Label for="descentRate" class="form-label">Скорость спуска (м/с):</Label>
|
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||||
<Input
|
<Label for="descentRate" class="form-label">Скорость спуска (м/с):</Label>
|
||||||
type="number"
|
<Input
|
||||||
id="descentRate"
|
type="number"
|
||||||
class="form-control-sm"
|
id="descentRate"
|
||||||
bind:value={$FlightParametersStore.descent_rate}
|
class="form-control-sm"
|
||||||
/>
|
bind:value={$FlightParametersStore.descent_rate}
|
||||||
</FormGroup>
|
/>
|
||||||
</div>
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="d-grid gap-1">
|
<div class="d-grid gap-1">
|
||||||
<Button color="outline-secondary" size="sm">Показать график высоты</Button>
|
<Button color="outline-secondary" size="sm">Показать график высоты</Button>
|
||||||
|
|
|
||||||
8
src/lib/components/PointEditor.svelte
Normal file
8
src/lib/components/PointEditor.svelte
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Datatable, Search, RowsPerPage, RowCount, Pagination } from '@vincjo/datatables'
|
||||||
|
import { Modal } from '@sveltestrap/sveltestrap';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
181
src/lib/components/PointListModal.svelte
Normal file
181
src/lib/components/PointListModal.svelte
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { TableHandler, Datatable } from '@vincjo/datatables'
|
||||||
|
import { Modal,
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputGroupText,
|
||||||
|
Icon,
|
||||||
|
Pagination,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
} from '@sveltestrap/sveltestrap';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import type { SavedPoint } from '$lib/types';
|
||||||
|
import { SavedPointsStore } from '$lib/stores';
|
||||||
|
import { getSavedPoints, savePoint, updatePoint, deletePoint } from '$lib/api/points';
|
||||||
|
|
||||||
|
export let isOpen: boolean = false;
|
||||||
|
export let onClose: () => void = () => {};
|
||||||
|
|
||||||
|
let points: SavedPoint[] = [];
|
||||||
|
const table = new TableHandler($SavedPointsStore, { rowsPerPage: 10 })
|
||||||
|
|
||||||
|
let selectedPoint: SavedPoint | null = null;
|
||||||
|
let newPoint: SavedPoint = { id: 0, name: '', lat: 0, lon: 0, alt: 0 };
|
||||||
|
let isEditing: boolean = false;
|
||||||
|
let modalTitle: string = 'Сохраненные точки';
|
||||||
|
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
points = await getSavedPoints();
|
||||||
|
SavedPointsStore.set(points);
|
||||||
|
});
|
||||||
|
|
||||||
|
export function openModal() {
|
||||||
|
isOpen = true;
|
||||||
|
modalTitle = 'Сохраненные точки';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeModal() {
|
||||||
|
isOpen = false;
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditPoint(point: SavedPoint) {
|
||||||
|
selectedPoint = point;
|
||||||
|
newPoint = { ...point };
|
||||||
|
isEditing = true;
|
||||||
|
modalTitle = 'Редактирование точки';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeletePoint(point: SavedPoint) {
|
||||||
|
deletePoint(point.id).then(() => {
|
||||||
|
points = points.filter(p => p.id !== point.id);
|
||||||
|
SavedPointsStore.set(points);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSavePoint() {
|
||||||
|
if (isEditing && selectedPoint) {
|
||||||
|
updatePoint(newPoint).then(updatedPoint => {
|
||||||
|
points = points.map(p => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||||
|
SavedPointsStore.set(points);
|
||||||
|
table.setRows(points);
|
||||||
|
resetForm();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
savePoint(newPoint).then(savedPoint => {
|
||||||
|
points.push(savedPoint);
|
||||||
|
points = [...points]; // Trigger reactivity
|
||||||
|
SavedPointsStore.set(points);
|
||||||
|
table.setRows(points);
|
||||||
|
resetForm();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetForm() {
|
||||||
|
selectedPoint = null;
|
||||||
|
newPoint = { id: 0, name: '', lat: 0, lon: 0, alt: 0 };
|
||||||
|
isEditing = false;
|
||||||
|
modalTitle = 'Сохраненные точки';
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (isOpen) {
|
||||||
|
SavedPointsStore.set(points);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal isOpen={isOpen} toggle={closeModal} size="lg" fade={false} backdrop={true} scrollable>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{modalTitle}</h5>
|
||||||
|
<button type="button" class="btn-close" on:click={closeModal} aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" >
|
||||||
|
<Datatable {table}>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Имя</th>
|
||||||
|
<th>Широта</th>
|
||||||
|
<th>Долгота</th>
|
||||||
|
<th>Высота</th>
|
||||||
|
<th class="fit"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each points as point (point.id)}
|
||||||
|
<tr>
|
||||||
|
<td>{point.name}</td>
|
||||||
|
<td>{point.lat}</td>
|
||||||
|
<td>{point.lon}</td>
|
||||||
|
<td>{point.alt}</td>
|
||||||
|
<td class="fit">
|
||||||
|
<Button color="primary" size="sm" on:click={() => handleEditPoint(point)}>
|
||||||
|
<Icon name="pencil" />
|
||||||
|
</Button>
|
||||||
|
<Button color="danger" size="sm" on:click={() => handleDeletePoint(point)}>
|
||||||
|
<Icon name="trash" />
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Datatable>
|
||||||
|
<Pagination aria-label="Page navigation">
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink previous on:click={() => table.setPage("previous")} />
|
||||||
|
</PaginationItem>
|
||||||
|
{#each table.pagesWithEllipsis as page}
|
||||||
|
<PaginationItem active={table.currentPage === page}>
|
||||||
|
<PaginationLink on:click={() => table.setPage(page)}>{page}</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
{/each}
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink next on:click={() => table.setPage("next")} />
|
||||||
|
</PaginationItem>
|
||||||
|
</Pagination>
|
||||||
|
|
||||||
|
<!-- Form for adding/editing points -->
|
||||||
|
<div class="mt-2">
|
||||||
|
<h5>{isEditing ? 'Редактирование точки' : 'Добавить новую точку'}</h5>
|
||||||
|
<form on:submit|preventDefault={handleSavePoint}>
|
||||||
|
<div class="mb-2">
|
||||||
|
<Label for="name" class="small">Имя:</Label>
|
||||||
|
<Input class="form-control-sm" type="text" id="name" bind:value={newPoint.name} required />
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<FormGroup class="flex-grow-1">
|
||||||
|
<Label for="lat" class="small">Широта:</Label>
|
||||||
|
<Input class="form-control-sm" type="number" id="lat" bind:value={newPoint.lat} required />
|
||||||
|
<span class="form-text">Градусы</span>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup class="flex-grow-1">
|
||||||
|
<Label for="lon" class="small">Долгота:</Label>
|
||||||
|
<Input class="form-control-sm" type="number" id="lon" bind:value={newPoint.lon} required />
|
||||||
|
<span class="form-text">Градусы</span>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup class="flex-grow-1">
|
||||||
|
<Label for="alt" class="small">Высота:</Label>
|
||||||
|
<Input class="form-control-sm" type="number" id="alt" bind:value={newPoint.alt} required />
|
||||||
|
<span class="form-text">Метры над ур. моря</span>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" color="success">
|
||||||
|
{isEditing ? 'Обновить точку' : 'Сохранить точку'}
|
||||||
|
</Button>
|
||||||
|
{#if isEditing}
|
||||||
|
<Button type="button" color="secondary" on:click={resetForm} class="ms-2">Отмена</Button>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<Button color="secondary" on:click={closeModal}>Закрыть</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { LatLngExpression } from "leaflet";
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
|
|
||||||
import { getCsrfToken } from "./auth";
|
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";
|
import { PredictionStore, RawPredictionStore, writeLocalStorage } from "./stores";
|
||||||
|
|
||||||
function getLatestDataset() {
|
function getLatestDataset() {
|
||||||
|
|
@ -90,10 +90,10 @@ export const getForecast = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parsePrediction(prediction: PredictionStage[]): Prediction {
|
export function parsePrediction(prediction: PredictionStage[]): Prediction {
|
||||||
const flight_path: [number, number, number][] = [];
|
const flight_path: LatLngExpression[] = [];
|
||||||
const launch: { latlng: LatLngExpression; datetime: Date } = {} as any;
|
const launch: Point = {} as any;
|
||||||
const burst: { latlng: LatLngExpression; datetime: Date } = {} as any;
|
const burst: Point = {} as any;
|
||||||
const landing: { latlng: LatLngExpression; datetime: Date } = {} as any;
|
const landing: Point = {} as any;
|
||||||
|
|
||||||
const ascent = prediction[0].trajectory;
|
const ascent = prediction[0].trajectory;
|
||||||
const descent = prediction[1].trajectory;
|
const descent = prediction[1].trajectory;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import type { FlightParameters, RawTelemetry, Telemetry } from "./types";
|
import type { FlightParameters, RawTelemetry, Telemetry } from "./types";
|
||||||
import type { RawPrediction, Prediction } from "./types";
|
import type { RawPrediction, Prediction } from "./types";
|
||||||
|
import type { SavedPoint } from "./types";
|
||||||
|
|
||||||
export const readLocalStorage = <T>(key: string, defaultValue: T): T => {
|
export const readLocalStorage = <T>(key: string, defaultValue: T): T => {
|
||||||
const item = localStorage.getItem(key);
|
const item = localStorage.getItem(key);
|
||||||
|
|
@ -67,3 +68,5 @@ export const RawPredictionStore = writable<RawPrediction>(
|
||||||
export const PredictionStore = writable<Prediction>(
|
export const PredictionStore = writable<Prediction>(
|
||||||
readLocalStorage<Prediction>("prediction", {} as Prediction)
|
readLocalStorage<Prediction>("prediction", {} as Prediction)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SavedPointsStore = writable<SavedPoint[]>([]);
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,11 @@ export interface FlightParameters {
|
||||||
launch_longitude: number;
|
launch_longitude: number;
|
||||||
profile: (typeof PROFILE_MAP)[ProfileName];
|
profile: (typeof PROFILE_MAP)[ProfileName];
|
||||||
version: number;
|
version: number;
|
||||||
|
start_point?: string; // Optional, used for saved points
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Point {
|
export interface Point {
|
||||||
latlng: LatLngLiteral & { alt: number };
|
latlng: LatLngLiteral & { alt?: number };
|
||||||
datetime: Date;
|
datetime: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,3 +84,11 @@ export interface Prediction {
|
||||||
profile: string;
|
profile: string;
|
||||||
flight_time: number;
|
flight_time: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SavedPoint {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
alt: number;
|
||||||
|
}
|
||||||
|
|
@ -3,15 +3,14 @@
|
||||||
import ControlPanel from "$lib/components/ControlPanel.svelte";
|
import ControlPanel from "$lib/components/ControlPanel.svelte";
|
||||||
import Navbar from "$lib/components/Navbar.svelte";
|
import Navbar from "$lib/components/Navbar.svelte";
|
||||||
import PanelContainer from "$lib/components/PanelContainer.svelte";
|
import PanelContainer from "$lib/components/PanelContainer.svelte";
|
||||||
import TelemetryPanel from '$lib/components/TelemetryPanel.svelte';
|
|
||||||
import ScenarioPanel from "$lib/components/ScenarioPanel.svelte";
|
import ScenarioPanel from "$lib/components/ScenarioPanel.svelte";
|
||||||
import TabComponent from "$lib/components/TabComponent.svelte";
|
import TabComponent from "$lib/components/TabComponent.svelte";
|
||||||
|
import PointListModal from "$lib/components/PointListModal.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { PredictionStore } from "$lib/stores";
|
import { PredictionStore } from "$lib/stores";
|
||||||
import { Modal, Icon } from "@sveltestrap/sveltestrap";
|
import { addToast, removeToast } from "$lib/components/Toast.svelte";
|
||||||
import Toast, { addToast, removeToast } from "$lib/components/Toast.svelte";
|
|
||||||
import ToastContainer 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 map: Map | null = null;
|
||||||
let panelContainer: PanelContainer | null = null;
|
let panelContainer: PanelContainer | null = null;
|
||||||
|
|
@ -19,6 +18,8 @@
|
||||||
let selectionToastId: string | null = null;
|
let selectionToastId: string | null = null;
|
||||||
let activeTab: 'control' | 'scenario' | 'settings' | 'about' = 'scenario';
|
let activeTab: 'control' | 'scenario' | 'settings' | 'about' = 'scenario';
|
||||||
|
|
||||||
|
let pointListModal: PointListModal | null = null;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
PredictionStore.subscribe((data) => {
|
PredictionStore.subscribe((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|
@ -65,6 +66,16 @@
|
||||||
selectionToastId = null;
|
selectionToastId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleClickPointListModal() {
|
||||||
|
if (map) {
|
||||||
|
map.stopSelection();
|
||||||
|
console.log("Selection mode disabled");
|
||||||
|
}
|
||||||
|
pointListModal?.openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
@ -73,7 +84,7 @@
|
||||||
<PanelContainer bind:this={panelContainer} >
|
<PanelContainer bind:this={panelContainer} >
|
||||||
<TabComponent
|
<TabComponent
|
||||||
tabs={[
|
tabs={[
|
||||||
{ id: 'scenario', icon: 'activity', label: 'Сценарий' },
|
{ id: 'scenario', icon: 'file-earmark-play', label: 'Сценарий' },
|
||||||
{ id: 'control', icon: 'sliders', label: 'Условия' },
|
{ id: 'control', icon: 'sliders', label: 'Условия' },
|
||||||
{ id: 'settings', icon: 'gear', label: 'Настройки' },
|
{ id: 'settings', icon: 'gear', label: 'Настройки' },
|
||||||
{ id: 'about', icon: 'info-circle', label: 'О проекте' }
|
{ id: 'about', icon: 'info-circle', label: 'О проекте' }
|
||||||
|
|
@ -83,7 +94,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if activeTab === 'control'}
|
{#if activeTab === 'control'}
|
||||||
<ControlPanel {handleClickSelectOnMap} bind:this={controlPanel} />
|
<ControlPanel {handleClickSelectOnMap} {handleClickPointListModal} bind:this={controlPanel} />
|
||||||
{:else if activeTab === 'scenario'}
|
{:else if activeTab === 'scenario'}
|
||||||
<ScenarioPanel />
|
<ScenarioPanel />
|
||||||
{:else if activeTab === 'settings'}
|
{:else if activeTab === 'settings'}
|
||||||
|
|
@ -94,5 +105,6 @@
|
||||||
</div>
|
</div>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
<PointListModal bind:this={pointListModal} />
|
||||||
</Map>
|
</Map>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,15 @@
|
||||||
box-shadow: none !important;
|
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)
|
@media (max-width: 767.98px)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue