Scenario system & point editor rework
This commit is contained in:
parent
7d01fce094
commit
19f969c18c
13 changed files with 1010 additions and 694 deletions
19
src/lib/api/scenarios.ts
Normal file
19
src/lib/api/scenarios.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* API functions for SavedScenario */
|
||||
import type { SavedScenario } from "$lib/types";
|
||||
import { getAPI, postAPI, putAPI, deleteAPI } from "./base";
|
||||
|
||||
export function getSavedScenarios(): Promise<SavedScenario[]> {
|
||||
return getAPI<SavedScenario[]>("/saved-templates/");
|
||||
}
|
||||
|
||||
export function saveScenario(template: SavedScenario): Promise<SavedScenario> {
|
||||
return postAPI<SavedScenario>("/saved-templates/", template);
|
||||
}
|
||||
|
||||
export function updateScenario(template: SavedScenario): Promise<SavedScenario> {
|
||||
return putAPI<SavedScenario>(`/saved-templates/${template.id}/`, template);
|
||||
}
|
||||
|
||||
export function deleteScenario(id: number): Promise<void> {
|
||||
return deleteAPI<void>(`/saved-templates/${id}/`);
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
/* API functions for SavedScenarioTemplate */
|
||||
import type { SavedScenarioTemplate } from "$lib/types";
|
||||
import { getAPI, postAPI, putAPI, deleteAPI } from "./base";
|
||||
|
||||
export function getSavedScenarioTemplates(): Promise<SavedScenarioTemplate[]> {
|
||||
return getAPI<SavedScenarioTemplate[]>("/saved-templates/");
|
||||
}
|
||||
|
||||
export function saveScenarioTemplate(template: SavedScenarioTemplate): Promise<SavedScenarioTemplate> {
|
||||
return postAPI<SavedScenarioTemplate>("/saved-templates/", template);
|
||||
}
|
||||
|
||||
export function updateScenarioTemplate(template: SavedScenarioTemplate): Promise<SavedScenarioTemplate> {
|
||||
return putAPI<SavedScenarioTemplate>(`/saved-templates/${template.id}/`, template);
|
||||
}
|
||||
|
||||
export function deleteScenarioTemplate(id: number): Promise<void> {
|
||||
return deleteAPI<void>(`/saved-templates/${id}/`);
|
||||
}
|
||||
|
|
@ -1,104 +1,134 @@
|
|||
<script lang="ts">
|
||||
/*
|
||||
Component Naming and Style Conventions:
|
||||
|
||||
1. **State Variables (`$state`)**:
|
||||
- Use camelCase.
|
||||
- No special prefixes are needed as `$state` already marks them as reactive state.
|
||||
- Example: `let isCollapsed = $state(false);`
|
||||
|
||||
2. **Derived State (`$derived`)**:
|
||||
- Use camelCase.
|
||||
- No special prefixes are needed as `$derived` already marks them as reactive derived state.
|
||||
- Example: `let inputLat = $derived(...)`
|
||||
|
||||
3. **Component Instance References**:
|
||||
- Use camelCase and suffix with `Ref`.
|
||||
- Example: `let PointEditorRef: PointEditor | null = null;`
|
||||
|
||||
4. **Event Handlers**:
|
||||
- Use `handle<EventName>` or `handle<Element><Event>` naming.
|
||||
- Example: `function handleToggleCollapse() { ... }`
|
||||
|
||||
5. **Props**:
|
||||
- For event callback props, use `on<EventName>`.
|
||||
- Example: `let { onSelectOnMapClick = () => {} }: Props = $props();`
|
||||
|
||||
6. **HTML Element IDs**:
|
||||
- Use kebab-case.
|
||||
- Prefix with a component-specific identifier to avoid global scope conflicts.
|
||||
- Example: `id="cp-start-time"` (cp for ControlPanel)
|
||||
|
||||
7. **Stores**:
|
||||
- Use PascalCase and suffix with `Store`.
|
||||
- Example: `import { SavedPointsStore } from '$lib/stores';`
|
||||
- The reactive Svelte store prefix `$` is used as standard.
|
||||
*/
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
FormGroup,
|
||||
Label,
|
||||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Icon,
|
||||
Label,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import PointListModal from "$lib/components/PointListModal.svelte";
|
||||
import { getSavedPoints, updatePoint } from "$lib/api/points";
|
||||
import { addToast } from "$lib/components/Toast.svelte";
|
||||
import PointEditor from "$lib/components/PointEditor.svelte";
|
||||
import SelectSearchable from "$lib/components/SelectSearchable.svelte";
|
||||
import { getForecast } from "$lib/prediction";
|
||||
import type { FlightParameters, ProfileName, ProfileIdentifier, SavedPoint } from "$lib/types";
|
||||
import { PROFILE_MAP, PROFILE_NAMES } from "$lib/types";
|
||||
import { SavedPointsStore, FlightParametersStore, writeLocalStorage } from "$lib/stores";
|
||||
import { onMount } from "svelte";
|
||||
import { addToast } from "$lib/components/Toast.svelte";
|
||||
import { getSavedPoints, savePoint, updatePoint, deletePoint } from "$lib/api/points";
|
||||
|
||||
// TODO: Move to $lib/utils/datetime.js
|
||||
// function getCurrentDateTime() {
|
||||
// const now = new Date();
|
||||
// return {
|
||||
// date: now.toISOString().split("T")[0],
|
||||
// time: now.toISOString().split("T")[1].split(".")[0]
|
||||
// };
|
||||
// }
|
||||
|
||||
// TODO: Move to $lib/utils/validation.js
|
||||
// function validateCoordinates(lat: string, lng: string): { isValid: boolean; lat?: number; lng?: number } {
|
||||
// const latNum = parseFloat(lat);
|
||||
// const lngNum = parseFloat(lng);
|
||||
// if (isNaN(latNum) || isNaN(lngNum)) {
|
||||
// return { isValid: false };
|
||||
// }
|
||||
// return { isValid: true, lat: latNum, lng: lngNum };
|
||||
// }
|
||||
|
||||
// TODO: Move to $lib/components/PredictionService.js
|
||||
// async function handlePredictionRequest(params: FlightParameters) {
|
||||
// try {
|
||||
// const response = await getForecast(params);
|
||||
// // Emit event or update store
|
||||
// return response;
|
||||
// } catch (error) {
|
||||
// console.error("Error fetching forecast:", error);
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
import { FlightParametersStore, SavedPointsStore, writeLocalStorage } from "$lib/stores";
|
||||
import { PROFILE_MAP, type FlightParameters, type ProfileName, type SavedPoint } from "$lib/types";
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
handleClickSelectOnMap?: () => void;
|
||||
onSelectOnMapClick?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
handleClickSelectOnMap = () => console.log("Select on map clicked"),
|
||||
}: Props = $props();
|
||||
let { onSelectOnMapClick = () => console.log("Select on map clicked") }: Props = $props();
|
||||
|
||||
// State
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
let pointListModal: PointListModal | null = null;
|
||||
|
||||
// Initialize date/time
|
||||
let isPointDirty = $state(false);
|
||||
const now = new Date();
|
||||
let startDate = $state(now.toISOString().split("T")[0]);
|
||||
let startTime = $state(now.toISOString().split("T")[1].split(".")[0]);
|
||||
let selectedPoint = $state($FlightParametersStore.start_point); // Default to custom point
|
||||
let selectedPointId = $state($FlightParametersStore.start_point);
|
||||
|
||||
// Coordinate inputs
|
||||
let inputLat = $derived($FlightParametersStore.start_point === -1
|
||||
// Component References
|
||||
let PointEditorRef: PointEditor | null = null;
|
||||
|
||||
// Derived State
|
||||
let inputLat = $derived(
|
||||
$FlightParametersStore.start_point === -1
|
||||
? $FlightParametersStore.launch_latitude.toFixed(6)
|
||||
: $SavedPointsStore.find(point => point.id === $FlightParametersStore.start_point)?.lat.toFixed(6) || "0.000000");
|
||||
let inputLng = $derived($FlightParametersStore.start_point === -1
|
||||
: $SavedPointsStore.find((point) => point.id === $FlightParametersStore.start_point)?.lat.toFixed(6) ||
|
||||
"0.000000",
|
||||
);
|
||||
let inputLng = $derived(
|
||||
$FlightParametersStore.start_point === -1
|
||||
? $FlightParametersStore.launch_longitude.toFixed(6)
|
||||
: $SavedPointsStore.find(point => point.id === $FlightParametersStore.start_point)?.lon.toFixed(6) || "0.000000");
|
||||
let inputAlt = $derived($FlightParametersStore.start_point === -1
|
||||
: $SavedPointsStore.find((point) => point.id === $FlightParametersStore.start_point)?.lon.toFixed(6) ||
|
||||
"0.000000",
|
||||
);
|
||||
let inputAlt = $derived(
|
||||
$FlightParametersStore.start_point === -1
|
||||
? $FlightParametersStore.launch_altitude.toFixed(2)
|
||||
: $SavedPointsStore.find(point => point.id === $FlightParametersStore.start_point)?.alt.toFixed(2) || "0.00");
|
||||
: $SavedPointsStore.find((point) => point.id === $FlightParametersStore.start_point)?.alt.toFixed(2) ||
|
||||
"0.00",
|
||||
);
|
||||
|
||||
function setToCustomOnChange() {
|
||||
if ($FlightParametersStore.start_point !== -1) {
|
||||
$FlightParametersStore.start_point = -1;
|
||||
// Lifecycle Hooks
|
||||
onMount(() => {
|
||||
getSavedPoints()
|
||||
.then((points) => SavedPointsStore.set(points))
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Error Loading Points",
|
||||
body: `Failed to load saved points: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
return [];
|
||||
});
|
||||
selectedPointId = $FlightParametersStore.start_point;
|
||||
});
|
||||
|
||||
function handleApplySelectedPoint() {
|
||||
if (selectedPointId && selectedPointId !== -1) {
|
||||
$FlightParametersStore.start_point = selectedPointId;
|
||||
isPointDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
function applySeletedPoint() {
|
||||
if (selectedPoint && selectedPoint !== -1) {
|
||||
$FlightParametersStore.start_point = selectedPoint;
|
||||
}
|
||||
export function handleSelectPointInModal(point: SavedPoint) {
|
||||
console.log("Selected point from modal:", point);
|
||||
selectedPointId = point.id;
|
||||
$FlightParametersStore.start_point = selectedPointId;
|
||||
isPointDirty = false;
|
||||
}
|
||||
|
||||
function saveCurrentPoint() {
|
||||
if (selectedPoint !== -1) {
|
||||
const point = $SavedPointsStore.find(p => p.id === selectedPoint);
|
||||
function handleSaveCurrentPoint() {
|
||||
if (selectedPointId !== -1) {
|
||||
const point = $SavedPointsStore.find((p) => p.id === selectedPointId);
|
||||
if (point) {
|
||||
point.lat = parseFloat(inputLat);
|
||||
point.lon = parseFloat(inputLng);
|
||||
point.alt = parseFloat(inputAlt);
|
||||
updatePoint(point)
|
||||
.then((updatedPoint) => {
|
||||
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||
|
|
@ -108,6 +138,7 @@
|
|||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||
color: "success",
|
||||
});
|
||||
isPointDirty = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
|
|
@ -119,81 +150,67 @@
|
|||
});
|
||||
}
|
||||
} else {
|
||||
pointListModal?.openModalAndCreate(null, {
|
||||
PointEditorRef?.openModalAndCreate(
|
||||
null,
|
||||
{
|
||||
id: 0,
|
||||
name: `Новая точка ${new Date().toLocaleString()}`,
|
||||
lat: parseFloat(inputLat),
|
||||
lon: parseFloat(inputLng),
|
||||
alt: parseFloat(inputAlt),
|
||||
}, true, onModalSave);
|
||||
},
|
||||
true,
|
||||
false,
|
||||
handleModalSave,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onModalSave(savedPoint: SavedPoint) {
|
||||
function handleModalSave(savedPoint: SavedPoint) {
|
||||
if (savedPoint) {
|
||||
$FlightParametersStore.start_point = savedPoint.id;
|
||||
selectedPoint = savedPoint.id;
|
||||
setToCustomOnChange();
|
||||
selectedPointId = savedPoint.id;
|
||||
isPointDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickPointListModal() {
|
||||
pointListModal?.openModal();
|
||||
function handlePointEditorOpen() {
|
||||
PointEditorRef?.openModal(true);
|
||||
}
|
||||
|
||||
// function applyCoordinatesFromInput() {
|
||||
// const lat = parseFloat(inputLat);
|
||||
// const lng = parseFloat(inputLng);
|
||||
// const alt = parseFloat(inputAlt);
|
||||
|
||||
// if (!isNaN(lat) && !isNaN(lng)) {
|
||||
// $FlightParametersStore.launch_latitude = lat;
|
||||
// $FlightParametersStore.launch_longitude = lng;
|
||||
// $FlightParametersStore.launch_altitude = alt || 0; // Default to 0 if alt is NaN
|
||||
// } else {
|
||||
// console.error("Invalid coordinate input");
|
||||
// // TODO: Show validation error to user
|
||||
// }
|
||||
// }
|
||||
|
||||
async function handleGetPrediction() {
|
||||
$FlightParametersStore.launch_datetime = `${startDate}T${startTime}Z`;
|
||||
async function handlePredictionRequest() {
|
||||
$FlightParametersStore.launch_latitude = parseFloat(inputLat);
|
||||
$FlightParametersStore.launch_longitude = parseFloat(inputLng);
|
||||
$FlightParametersStore.launch_altitude = parseFloat(inputAlt);
|
||||
writeLocalStorage<FlightParameters>("flightParameters", $FlightParametersStore);
|
||||
|
||||
getForecast($FlightParametersStore)
|
||||
.then((data) => {
|
||||
try {
|
||||
const data = await getForecast($FlightParametersStore, `${startDate}T${startTime}Z`);
|
||||
console.log("Forecast request successful:", data);
|
||||
addToast({
|
||||
header: "Forecast Request",
|
||||
body: "Forecast request successful!",
|
||||
color: "success",
|
||||
});
|
||||
// Handle the response data as needed
|
||||
})
|
||||
.catch((error) => {
|
||||
} catch (error: any) {
|
||||
console.error("Error getting forecast:", error);
|
||||
addToast({
|
||||
header: "Forecast Error",
|
||||
body: `Error getting forecast: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCollapse() {
|
||||
function handleToggleCollapse() {
|
||||
isCollapsed = !isCollapsed;
|
||||
}
|
||||
|
||||
// Exported functions for parent components
|
||||
// Public API
|
||||
export function updateLaunchPosition(lat: number, lng: number) {
|
||||
$FlightParametersStore.launch_latitude = lat;
|
||||
$FlightParametersStore.launch_longitude = lng;
|
||||
inputLat = lat.toFixed(6);
|
||||
inputLng = lng.toFixed(6);
|
||||
setToCustomOnChange();
|
||||
isPointDirty = true;
|
||||
}
|
||||
|
||||
export function getSelectedProfile() {
|
||||
|
|
@ -215,21 +232,6 @@
|
|||
export function togglePanel() {
|
||||
isCollapsed = !isCollapsed;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getSavedPoints()
|
||||
.then((points) => SavedPointsStore.set(points))
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Error Loading Points",
|
||||
body: `Failed to load saved points: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
return [];
|
||||
});
|
||||
selectedPoint = $FlightParametersStore.start_point;
|
||||
console.log("ControlPanel mounted", selectedPoint);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
|
|
@ -240,11 +242,11 @@
|
|||
<button
|
||||
type="button"
|
||||
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
||||
aria-label="Свернуть/развернуть параметры прогнозирования"
|
||||
onclick={toggleCollapse}
|
||||
aria-label="Свернуть/развернуть условия прогнозирования"
|
||||
onclick={handleToggleCollapse}
|
||||
>
|
||||
<b class="card-title mb-0 text-white p-0">Параметры прогнозирования</b>
|
||||
<Button class="p-0" size="sm" color="primary" onclick={toggleCollapse}>
|
||||
<b class="card-title mb-0 text-white p-0">Условия прогнозирования</b>
|
||||
<Button class="p-0" size="sm" color="primary" onclick={handleToggleCollapse}>
|
||||
{#if isCollapsed}
|
||||
<Icon name="caret-left-fill" class="text-white" />
|
||||
{:else}
|
||||
|
|
@ -258,28 +260,181 @@
|
|||
<CardBody>
|
||||
<div class="d-flex gap-2">
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="startTime" class="form-label">Время старта (UTC):</Label>
|
||||
<Input type="time" id="startTime" class="form-control-sm" bind:value={startTime} step="1" />
|
||||
<Label for="cp-start-time" class="form-label">Время старта (UTC):</Label>
|
||||
<Input type="time" id="cp-start-time" class="form-control-sm" bind:value={startTime} step="1" />
|
||||
</FormGroup>
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="startDate" class="form-label">Дата старта:</Label>
|
||||
<Input type="date" id="startDate" class="form-control-sm" bind:value={startDate} />
|
||||
<Label for="cp-start-date" class="form-label">Дата старта:</Label>
|
||||
<Input type="date" id="cp-start-date" class="form-control-sm" bind:value={startDate} />
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="flightProfile" class="form-label">Профиль полета:</Label>
|
||||
<Label for="cp-flight-profile" class="form-label">Профиль полета:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="flightProfile" bind:value={$FlightParametersStore.profile}>
|
||||
<optgroup label="Стандартные профили">
|
||||
<Input type="select" id="cp-flight-profile" bind:value={$FlightParametersStore.profile}>
|
||||
{#each Object.keys(PROFILE_MAP) as profileName}
|
||||
<option value={PROFILE_MAP[profileName as ProfileName]}>{profileName}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
<optgroup label="Пользовательские профили">
|
||||
<option>Custom</option>
|
||||
</optgroup>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="cp-start-point" class="form-label">Точка старта:</Label>
|
||||
<InputGroup size="sm">
|
||||
<div class="position-relative flex-grow-1">
|
||||
<SelectSearchable
|
||||
style="min-height: calc(1.5em + .5rem + 2px); padding: .25rem .5rem; font-size: .875rem;"
|
||||
id="cp-start-point"
|
||||
bind:selected={selectedPointId}
|
||||
options={$SavedPointsStore.map((point) => ({
|
||||
value: point.id,
|
||||
label:
|
||||
point.name +
|
||||
`${point.id == $FlightParametersStore.start_point && isPointDirty ? " (изменено)" : ""}`,
|
||||
}))}
|
||||
placeholder="Новая точка..."
|
||||
searchPlaceholder="Поиск по точкам..."
|
||||
on:change={() => {
|
||||
if (!isPointDirty) {
|
||||
$FlightParametersStore.start_point = selectedPointId;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white); z-index: 10; margin-right: 2rem;"
|
||||
on:click={() => {
|
||||
selectedPointId = -1;
|
||||
$FlightParametersStore.start_point = -1;
|
||||
}}
|
||||
disabled={selectedPointId === -1}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button color="success" size="sm" onclick={handleApplySelectedPoint} title="Apply Coordinates"
|
||||
>✓</Button
|
||||
>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<Button
|
||||
color="secondary flex-fill"
|
||||
size="sm"
|
||||
onclick={handlePointEditorOpen}
|
||||
title="Открыть список точек"
|
||||
>
|
||||
Все точки
|
||||
<Icon name="journal-bookmark-fill" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary flex-fill"
|
||||
size="sm"
|
||||
onclick={handleSaveCurrentPoint}
|
||||
title="Сохранить текущие координаты"
|
||||
disabled={!isPointDirty && selectedPointId !== -1}
|
||||
>
|
||||
{selectedPointId !== -1 ? "Обновить точку" : "Сохранить точку"}
|
||||
<Icon name="floppy2-fill" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="cp-latitude" class="form-label">Широта/Долгота:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input
|
||||
id="cp-latitude"
|
||||
type="text"
|
||||
bind:value={inputLat}
|
||||
placeholder="Latitude"
|
||||
on:change={() => {
|
||||
isPointDirty = true;
|
||||
}}
|
||||
/>
|
||||
<InputGroupText>/</InputGroupText>
|
||||
<Input
|
||||
id="cp-longitude"
|
||||
type="text"
|
||||
bind:value={inputLng}
|
||||
placeholder="Longitude"
|
||||
on:change={() => {
|
||||
isPointDirty = true;
|
||||
}}
|
||||
/>
|
||||
<Button color="secondary" size="sm" onclick={onSelectOnMapClick}>
|
||||
<Icon name="geo-alt-fill" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<div class="d-flex gap-2">
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="cp-start-height" class="form-label">Высота старта (м):</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="cp-start-height"
|
||||
class="form-control-sm"
|
||||
on:change={() => {
|
||||
isPointDirty = true;
|
||||
}}
|
||||
bind:value={inputAlt}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="cp-burst-altitude" class="form-label">Высота разрыва (м):</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="cp-burst-altitude"
|
||||
class="form-control-sm"
|
||||
bind:value={$FlightParametersStore.burst_altitude}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
{#if $FlightParametersStore.profile !== "custom_profile"}
|
||||
<div class="mb-2 d-flex gap-2">
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="cp-ascent-rate" class="form-label">Скорость подъема (м/с):</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="cp-ascent-rate"
|
||||
class="form-control-sm"
|
||||
bind:value={$FlightParametersStore.ascent_rate}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="cp-descent-rate" class="form-label">Скорость спуска (м/с):</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="cp-descent-rate"
|
||||
class="form-control-sm"
|
||||
bind:value={$FlightParametersStore.descent_rate}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
{:else}
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="cp-flight-profile" class="form-label">Пользовательский профиль:</Label>
|
||||
<InputGroup size="sm">
|
||||
<SelectSearchable
|
||||
id="cp-flight-profile"
|
||||
bind:selected={$FlightParametersStore.profile}
|
||||
options={Object.keys(PROFILE_MAP).map((profileName) => ({
|
||||
// stub, replace with actual profiles
|
||||
value: PROFILE_MAP[profileName as ProfileName],
|
||||
label: profileName,
|
||||
}))}
|
||||
placeholder="Выберите профиль..."
|
||||
searchPlaceholder="Поиск профилей..."
|
||||
on:change={() => {
|
||||
$FlightParametersStore.profile = $FlightParametersStore.profile;
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
|
|
@ -291,138 +446,12 @@
|
|||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="startPoint" class="form-label">Точка старта:</Label>
|
||||
<InputGroup size="sm">
|
||||
<div class="position-relative flex-grow-1">
|
||||
<SelectSearchable
|
||||
style="min-height: calc(1.5em + .5rem + 2px); padding: .25rem .5rem; font-size: .875rem;"
|
||||
id="startPoint"
|
||||
bind:selected={selectedPoint}
|
||||
options={$SavedPointsStore.map(point => ({
|
||||
value: point.id,
|
||||
label: point.name,
|
||||
}))}
|
||||
placeholder="Выберите точку старта"
|
||||
searchPlaceholder="Поиск точки..."
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white); z-index: 10; margin-right: 2rem;"
|
||||
on:click={() => {
|
||||
selectedPoint = -1;
|
||||
$FlightParametersStore.start_point = -1;
|
||||
}}
|
||||
disabled={selectedPoint === -1}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button color="success" size="sm" onclick={applySeletedPoint} title="Apply Coordinates">
|
||||
✓
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<Button
|
||||
color="secondary flex-fill"
|
||||
size="sm"
|
||||
onclick={handleClickPointListModal}
|
||||
title="Открыть список точек"
|
||||
>
|
||||
Все точки
|
||||
<Icon name="journal-bookmark-fill" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary flex-fill"
|
||||
size="sm"
|
||||
onclick={saveCurrentPoint}
|
||||
title="Сохранить текущие координаты"
|
||||
>
|
||||
Сохранить точку
|
||||
<Icon name="floppy2-fill" />
|
||||
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="latitude" class="form-label">Широта/Долгота:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input
|
||||
id="latitude"
|
||||
type="text"
|
||||
bind:value={inputLat}
|
||||
placeholder="Latitude"
|
||||
onchange={setToCustomOnChange}
|
||||
/>
|
||||
<InputGroupText>/</InputGroupText>
|
||||
<Input
|
||||
id="longitude"
|
||||
type="text"
|
||||
bind:value={inputLng}
|
||||
placeholder="Longitude"
|
||||
onchange={setToCustomOnChange}
|
||||
/>
|
||||
<Button color="secondary" size="sm" onclick={handleClickSelectOnMap}>
|
||||
<Icon name="geo-alt-fill" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<div class="d-flex gap-2">
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="startHeight" class="form-label">Высота старта (м):</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="startHeight"
|
||||
class="form-control-sm"
|
||||
onchange={setToCustomOnChange}
|
||||
bind:value={inputAlt}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="burstAltitude" class="form-label">Высота разрыва (м):</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="burstAltitude"
|
||||
class="form-control-sm"
|
||||
bind:value={$FlightParametersStore.burst_altitude}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
{#if $FlightParametersStore.profile !== "custom_profile"}
|
||||
<div class="mb-2 d-flex gap-2">
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="ascentRate" class="form-label">Скорость подъема (м/с):</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="ascentRate"
|
||||
class="form-control-sm"
|
||||
bind:value={$FlightParametersStore.ascent_rate}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="descentRate" class="form-label">Скорость спуска (м/с):</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="descentRate"
|
||||
class="form-control-sm"
|
||||
bind:value={$FlightParametersStore.descent_rate}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="d-grid gap-1">
|
||||
<Button color="outline-secondary" size="sm">Показать график высоты</Button>
|
||||
<Button size="sm" color="primary" onclick={handleGetPrediction}>Выполнить прогнозирование</Button>
|
||||
<Button size="sm" color="primary" onclick={handlePredictionRequest}>Выполнить прогнозирование</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
{/if}
|
||||
</Card>
|
||||
<PointListModal bind:this={pointListModal} />
|
||||
<PointEditor bind:this={PointEditorRef} onSelectPoint={handleSelectPointInModal} />
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
Pagination,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
InputGroup,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { onMount } from "svelte";
|
||||
import { addToast } from "$lib/components/Toast.svelte";
|
||||
|
|
@ -20,39 +21,116 @@
|
|||
import { getSavedPoints, savePoint, updatePoint, deletePoint } from "$lib/api/points";
|
||||
|
||||
// Props
|
||||
let { isOpen = $bindable(false), onClose = () => {}, onChange = () => {}, point} = $props();
|
||||
let {
|
||||
isOpen = $bindable(false),
|
||||
onClose = () => {},
|
||||
onSave = (p: SavedPoint) => {},
|
||||
onSelectPoint = (p: SavedPoint) => {},
|
||||
showTable = false,
|
||||
point = null,
|
||||
editor = false,
|
||||
closeOnSave = false,
|
||||
closeOnDelete = false,
|
||||
} = $props();
|
||||
|
||||
// Runes
|
||||
let selectedPoint = $derived<SavedPoint>(point);
|
||||
let selectedPoint = $derived<SavedPoint | null>(point);
|
||||
let newPoint = $state<SavedPoint>({ id: 0, name: "", lat: 0, lon: 0, alt: 0 });
|
||||
|
||||
let isEditing = $state(editor);
|
||||
let isAlertVisible = $state(false);
|
||||
let isConfirmationVisible = $state(false);
|
||||
let alertText = $state("");
|
||||
let closeOnSave_ = $state(closeOnSave);
|
||||
|
||||
// Table handler
|
||||
let table = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 10 }));
|
||||
let search = $derived(table.createSearch(["name"]));
|
||||
|
||||
$effect(() => {
|
||||
onChange();
|
||||
if (showTable) {
|
||||
getSavedPoints().then((pts) => {
|
||||
$SavedPointsStore = pts;
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
});
|
||||
}
|
||||
if (editor && point) {
|
||||
selectedPoint = point;
|
||||
newPoint = { ...point };
|
||||
isEditing = true;
|
||||
}
|
||||
});
|
||||
|
||||
// On mount, fetch points
|
||||
onMount(async () => {
|
||||
if (showTable) {
|
||||
const pts = await getSavedPoints();
|
||||
$SavedPointsStore = pts;
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
}
|
||||
});
|
||||
|
||||
// Modal controls
|
||||
export function openModal() {
|
||||
export function openModal(table_: boolean = false) {
|
||||
showTable = table_;
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
export function openModalAndCreate(
|
||||
point: SavedPoint | null = null,
|
||||
coordinates: SavedPoint = { id: 0, name: "", lat: 0, lon: 0, alt: 0 },
|
||||
close: boolean = false,
|
||||
table_: boolean = false,
|
||||
onSaveCallback: (point: SavedPoint) => void = () => {},
|
||||
) {
|
||||
if (point) {
|
||||
selectedPoint = point;
|
||||
newPoint = { ...point };
|
||||
isEditing = true;
|
||||
} else {
|
||||
selectedPoint = null;
|
||||
newPoint = coordinates || { id: 0, name: "", lat: 0, lon: 0, alt: 0 };
|
||||
isEditing = false;
|
||||
}
|
||||
showTable = table_;
|
||||
isOpen = true;
|
||||
closeOnSave_ = close;
|
||||
onSave = onSaveCallback;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
isOpen = false;
|
||||
if (closeOnSave_ != closeOnSave) {
|
||||
closeOnSave = closeOnSave_;
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleDeletePoint(point: SavedPoint) {
|
||||
function handleEditPoint(point: SavedPoint) {
|
||||
selectedPoint = point;
|
||||
newPoint = { ...point };
|
||||
isEditing = true;
|
||||
}
|
||||
|
||||
function confirmDeletePoint(point: SavedPoint) {
|
||||
selectedPoint = point;
|
||||
isConfirmationVisible = true;
|
||||
}
|
||||
|
||||
function handleDeletePoint(point: SavedPoint | null) {
|
||||
if (!point) return;
|
||||
deletePoint(point.id)
|
||||
.then(() => {
|
||||
$SavedPointsStore = $SavedPointsStore.filter((p) => p.id !== point.id);
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: "Точка удалена",
|
||||
body: `Точка "${point.name}" успешно удалена.`,
|
||||
color: "success",
|
||||
});
|
||||
if (closeOnDelete) {
|
||||
closeModal();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при удалении точки: ${error.message}`);
|
||||
|
|
@ -60,8 +138,9 @@
|
|||
});
|
||||
}
|
||||
|
||||
function handleSavePoint() {
|
||||
updatePoint(selectedPoint)
|
||||
export function handleSavePoint() {
|
||||
if (isEditing && selectedPoint) {
|
||||
updatePoint(newPoint)
|
||||
.then((updatedPoint) => {
|
||||
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
|
|
@ -71,10 +150,35 @@
|
|||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||
color: "success",
|
||||
});
|
||||
if (closeOnSave_) {
|
||||
closeModal();
|
||||
}
|
||||
onSave(updatedPoint);
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при обновлении точки: ${error.message}`);
|
||||
});
|
||||
} else {
|
||||
savePoint(newPoint)
|
||||
.then((savedPoint) => {
|
||||
$SavedPointsStore = [...$SavedPointsStore, savedPoint];
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: "Точка сохранена",
|
||||
body: `Точка "${savedPoint.name}" успешно сохранена.`,
|
||||
color: "success",
|
||||
});
|
||||
if (closeOnSave_) {
|
||||
closeModal();
|
||||
}
|
||||
onSave(savedPoint);
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при сохранении точки: ${error.message}`);
|
||||
console.error("Ошибка при сохранении точки:", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function showAlert(message: string) {
|
||||
|
|
@ -88,19 +192,111 @@
|
|||
}
|
||||
|
||||
export function resetForm() {
|
||||
selectedPoint = null;
|
||||
newPoint = { id: 0, name: "", lat: 0, lon: 0, alt: 0 };
|
||||
isEditing = false;
|
||||
hideAlert();
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {isOpen} toggle={closeModal} size="lg" fade={false} backdrop={true} scrollable class={ isConfirmationVisible ? "modal-tinted" : ""}>
|
||||
<Modal
|
||||
{isOpen}
|
||||
toggle={closeModal}
|
||||
size="lg"
|
||||
fade={false}
|
||||
backdrop={true}
|
||||
scrollable
|
||||
class={isConfirmationVisible ? "modal-tinted" : ""}
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Редактирование точки</h5>
|
||||
<h5 class="modal-title">{isEditing ? "Редактирование точки" : showTable ? "Сохраненные точки" : "Добавить новую точку"}</h5>
|
||||
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{#if showTable}
|
||||
<div class="position-relative mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
class="form-control-sm pe-5"
|
||||
placeholder="Поиск по названию..."
|
||||
bind:value={search.value}
|
||||
oninput={() => search.set()}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y me-2 rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white);"
|
||||
onclick={() => {
|
||||
search.value = "";
|
||||
search.set();
|
||||
}}
|
||||
disabled={!search.value}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<div bind:this={table.element} class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название точки</th>
|
||||
<th>Широта</th>
|
||||
<th>Долгота</th>
|
||||
<th>Высота</th>
|
||||
<th class="fit">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each table.rows as row}
|
||||
<tr>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.lat} °</td>
|
||||
<td>{row.lon} °</td>
|
||||
<td>{row.alt} м</td>
|
||||
<td class="fit">
|
||||
<InputGroup size="sm" class="d-flex gap-1 flex-nowrap">
|
||||
<Button
|
||||
color="success"
|
||||
size="sm"
|
||||
onclick={() => {onSelectPoint(row); closeModal();}}>
|
||||
✓
|
||||
</Button>
|
||||
<Button color="primary" size="sm" onclick={() => handleEditPoint(row)}>
|
||||
<Icon name="pencil" />
|
||||
</Button>
|
||||
<Button color="danger" size="sm" onclick={() => confirmDeletePoint(row)}>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination aria-label="Page navigation" size="sm">
|
||||
<PaginationItem>
|
||||
<PaginationLink previous onclick={() => table.setPage("previous")} />
|
||||
</PaginationItem>
|
||||
{#each table.pagesWithEllipsis as page}
|
||||
<PaginationItem active={table.currentPage === page}>
|
||||
<PaginationLink onclick={() => table.setPage(page)}>{page}</PaginationLink>
|
||||
</PaginationItem>
|
||||
{/each}
|
||||
<PaginationItem>
|
||||
<PaginationLink next onclick={() => table.setPage("next")} />
|
||||
</PaginationItem>
|
||||
</Pagination>
|
||||
{/if}
|
||||
|
||||
{#if showTable && (isEditing || newPoint.lat || newPoint.lon)}<hr />{/if}
|
||||
|
||||
<!-- Form for adding/editing points -->
|
||||
<div>
|
||||
<h5>{"Редактирование точки"}</h5>
|
||||
{#if showTable}
|
||||
<h5>{isEditing ? "Редактирование точки" : "Добавить новую точку"}</h5>
|
||||
{/if}
|
||||
<Alert
|
||||
color="danger"
|
||||
isOpen={isAlertVisible}
|
||||
|
|
@ -119,7 +315,7 @@
|
|||
>
|
||||
<div class="mb-2">
|
||||
<Label for="name" class="small">Название точки:</Label>
|
||||
<Input class="form-control-sm" type="text" id="name" bind:value={selectedPoint.name} required />
|
||||
<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">
|
||||
|
|
@ -129,7 +325,7 @@
|
|||
type="number"
|
||||
step="any"
|
||||
id="lat"
|
||||
bind:value={selectedPoint.lat}
|
||||
bind:value={newPoint.lat}
|
||||
required
|
||||
/>
|
||||
<span class="form-text">Градусы</span>
|
||||
|
|
@ -141,7 +337,7 @@
|
|||
type="number"
|
||||
step="any"
|
||||
id="lon"
|
||||
bind:value={selectedPoint.lon}
|
||||
bind:value={newPoint.lon}
|
||||
required
|
||||
/>
|
||||
<span class="form-text">Градусы</span>
|
||||
|
|
@ -153,7 +349,7 @@
|
|||
type="number"
|
||||
step="any"
|
||||
id="alt"
|
||||
bind:value={selectedPoint.alt}
|
||||
bind:value={newPoint.alt}
|
||||
required
|
||||
/>
|
||||
<span class="form-text">Метры над ур. моря</span>
|
||||
|
|
@ -161,18 +357,29 @@
|
|||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<Button type="submit" color="success" size="sm">
|
||||
Обновить точку
|
||||
{isEditing ? "Обновить точку" : "Сохранить точку"}
|
||||
</Button>
|
||||
{#if isEditing}
|
||||
<Button size="sm" type="button" color="secondary" onclick={resetForm}>Отмена</Button>
|
||||
{/if}
|
||||
<span class="flex-grow-1"></span>
|
||||
<Button
|
||||
type="button"
|
||||
color="danger"
|
||||
size="sm"
|
||||
onclick={() => isConfirmationVisible = true}
|
||||
>
|
||||
{#if isEditing}
|
||||
<Button color="danger" size="sm" type="button" onclick={() => confirmDeletePoint(newPoint)}>
|
||||
Удалить точку
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onclick={() => {
|
||||
resetForm();
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
Закрыть без сохранения
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,347 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { TableHandler } from "@vincjo/datatables";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
FormGroup,
|
||||
Label,
|
||||
Input,
|
||||
Alert,
|
||||
Icon,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { onMount } from "svelte";
|
||||
import { addToast } from "$lib/components/Toast.svelte";
|
||||
import type { SavedPoint } from "$lib/types";
|
||||
import { SavedPointsStore } from "$lib/stores";
|
||||
import ConfirmationPrompt from "./ConfirmationPrompt.svelte";
|
||||
import { getSavedPoints, savePoint, updatePoint, deletePoint } from "$lib/api/points";
|
||||
|
||||
// Props
|
||||
let {
|
||||
isOpen = $bindable(false),
|
||||
onClose = () => {},
|
||||
onChange = () => {},
|
||||
onSave = () => {},
|
||||
point = null,
|
||||
coordinates = { id: 0, name: "", lat: 0, lon: 0, alt: 0 },
|
||||
} = $props();
|
||||
|
||||
// Runes
|
||||
let selectedPoint = $state<SavedPoint | null>(point);
|
||||
let newPoint = $state<SavedPoint>(coordinates as SavedPoint);
|
||||
let closeOnSave = $state(false);
|
||||
let isEditing = $state(false);
|
||||
let isAlertVisible = $state(false);
|
||||
let isConfirmationVisible = $state(false);
|
||||
let alertText = $state("");
|
||||
|
||||
// Table handler
|
||||
let table = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 10 }));
|
||||
let search = $derived(table.createSearch(["name"]));
|
||||
|
||||
$effect(() => {
|
||||
onChange();
|
||||
});
|
||||
|
||||
// On mount, fetch points
|
||||
onMount(async () => {
|
||||
const pts = await getSavedPoints();
|
||||
$SavedPointsStore = pts;
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
});
|
||||
|
||||
// Modal controls
|
||||
export function openModal() {
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
export function openModalAndCreate(
|
||||
point: SavedPoint | null = null,
|
||||
coordinates: SavedPoint = { id: 0, name: "", lat: 0, lon: 0, alt: 0 },
|
||||
close: boolean = false,
|
||||
onSaveCallback: (point: SavedPoint) => void = () => {}
|
||||
) {
|
||||
if (point) {
|
||||
selectedPoint = point;
|
||||
newPoint = { ...point };
|
||||
isEditing = true;
|
||||
} else {
|
||||
selectedPoint = null;
|
||||
newPoint = coordinates || { id: 0, name: "", lat: 0, lon: 0, alt: 0 };
|
||||
isEditing = false;
|
||||
}
|
||||
isOpen = true;
|
||||
closeOnSave = close;
|
||||
onSave = onSaveCallback;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
isOpen = false;
|
||||
closeOnSave = false;
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleEditPoint(point: SavedPoint) {
|
||||
selectedPoint = point;
|
||||
newPoint = { ...point };
|
||||
isEditing = true;
|
||||
}
|
||||
|
||||
function confirmDeletePoint(point: SavedPoint) {
|
||||
selectedPoint = point;
|
||||
isConfirmationVisible = true;
|
||||
}
|
||||
|
||||
function handleDeletePoint(point: SavedPoint | null) {
|
||||
if (!point) return;
|
||||
deletePoint(point.id)
|
||||
.then(() => {
|
||||
$SavedPointsStore = $SavedPointsStore.filter((p) => p.id !== point.id);
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
addToast({
|
||||
header: "Точка удалена",
|
||||
body: `Точка "${point.name}" успешно удалена.`,
|
||||
color: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при удалении точки: ${error.message}`);
|
||||
console.error("Ошибка при удалении точки:", error);
|
||||
});
|
||||
}
|
||||
|
||||
export function handleSavePoint() {
|
||||
if (isEditing && selectedPoint) {
|
||||
updatePoint(newPoint)
|
||||
.then((updatedPoint) => {
|
||||
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: "Точка обновлена",
|
||||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||
color: "success",
|
||||
});
|
||||
if (closeOnSave) {
|
||||
closeModal();
|
||||
}
|
||||
onSave(updatedPoint);
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при обновлении точки: ${error.message}`);
|
||||
});
|
||||
} else {
|
||||
savePoint(newPoint)
|
||||
.then((savedPoint) => {
|
||||
$SavedPointsStore = [...$SavedPointsStore, savedPoint];
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: "Точка сохранена",
|
||||
body: `Точка "${savedPoint.name}" успешно сохранена.`,
|
||||
color: "success",
|
||||
});
|
||||
if (closeOnSave) {
|
||||
closeModal();
|
||||
}
|
||||
onSave(savedPoint);
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при сохранении точки: ${error.message}`);
|
||||
console.error("Ошибка при сохранении точки:", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function showAlert(message: string) {
|
||||
isAlertVisible = true;
|
||||
alertText = message;
|
||||
}
|
||||
|
||||
export function hideAlert() {
|
||||
isAlertVisible = false;
|
||||
alertText = "";
|
||||
}
|
||||
|
||||
export function resetForm() {
|
||||
selectedPoint = null;
|
||||
newPoint = { id: 0, name: "", lat: 0, lon: 0, alt: 0 };
|
||||
isEditing = false;
|
||||
hideAlert();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
{isOpen}
|
||||
toggle={closeModal}
|
||||
size="lg"
|
||||
fade={false}
|
||||
backdrop={true}
|
||||
scrollable
|
||||
class={isConfirmationVisible ? "modal-tinted" : ""}
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Сохраненные точки</h5>
|
||||
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="position-relative mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
class="form-control-sm pe-5"
|
||||
placeholder="Поиск по названию..."
|
||||
bind:value={search.value}
|
||||
oninput={() => search.set()}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y me-2 rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white);"
|
||||
onclick={() => {
|
||||
search.value = "";
|
||||
search.set();
|
||||
}}
|
||||
disabled={!search.value}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<div bind:this={table.element} class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название точки</th>
|
||||
<th>Широта</th>
|
||||
<th>Долгота</th>
|
||||
<th>Высота</th>
|
||||
<th class="fit"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each table.rows as row}
|
||||
<tr>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.lat} °</td>
|
||||
<td>{row.lon} °</td>
|
||||
<td>{row.alt} м</td>
|
||||
<td class="fit">
|
||||
<Button color="primary" size="sm" onclick={() => handleEditPoint(row)}>
|
||||
<Icon name="pencil" />
|
||||
</Button>
|
||||
<Button color="danger" size="sm" onclick={() => confirmDeletePoint(row)}>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination aria-label="Page navigation" size="sm">
|
||||
<PaginationItem>
|
||||
<PaginationLink previous onclick={() => table.setPage("previous")} />
|
||||
</PaginationItem>
|
||||
{#each table.pagesWithEllipsis as page}
|
||||
<PaginationItem active={table.currentPage === page}>
|
||||
<PaginationLink onclick={() => table.setPage(page)}>{page}</PaginationLink>
|
||||
</PaginationItem>
|
||||
{/each}
|
||||
<PaginationItem>
|
||||
<PaginationLink next onclick={() => table.setPage("next")} />
|
||||
</PaginationItem>
|
||||
</Pagination>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Form for adding/editing points -->
|
||||
<div>
|
||||
<h5>{isEditing ? "Редактирование точки" : "Добавить новую точку"}</h5>
|
||||
<Alert
|
||||
color="danger"
|
||||
isOpen={isAlertVisible}
|
||||
toggle={() => (isAlertVisible = false)}
|
||||
fade={false}
|
||||
class="mb-2"
|
||||
>
|
||||
<Icon name="exclamation-triangle" class="me-2" />
|
||||
{alertText}
|
||||
</Alert>
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.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"
|
||||
step="any"
|
||||
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"
|
||||
step="any"
|
||||
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"
|
||||
step="any"
|
||||
id="alt"
|
||||
bind:value={newPoint.alt}
|
||||
required
|
||||
/>
|
||||
<span class="form-text">Метры над ур. моря</span>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<Button type="submit" color="success" size="sm">
|
||||
{isEditing ? "Обновить точку" : "Сохранить точку"}
|
||||
</Button>
|
||||
{#if isEditing}
|
||||
<Button size="sm" type="button" color="secondary" onclick={resetForm}>Отмена</Button>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmationPrompt
|
||||
isOpen={isConfirmationVisible}
|
||||
title="Подтвердите удаление"
|
||||
confirmText="Удалить"
|
||||
cancelText="Отмена"
|
||||
confirmVariant="danger"
|
||||
onconfirm={() => {
|
||||
isConfirmationVisible = false;
|
||||
handleDeletePoint(selectedPoint);
|
||||
}}
|
||||
oncancel={() => {
|
||||
isConfirmationVisible = false;
|
||||
}}
|
||||
>
|
||||
<p>Вы уверены, что хотите удалить эту точку?</p>
|
||||
</ConfirmationPrompt>
|
||||
262
src/lib/components/ScenarioEditor.svelte
Normal file
262
src/lib/components/ScenarioEditor.svelte
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
<script lang="ts">
|
||||
import { TableHandler } from "@vincjo/datatables";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
FormGroup,
|
||||
Label,
|
||||
Input,
|
||||
Alert,
|
||||
Icon,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { onMount } from "svelte";
|
||||
import { addToast } from "$lib/components/Toast.svelte";
|
||||
import type { SavedScenario } from "$lib/types";
|
||||
import { FlightParametersStore, SavedScenarioStore } from "$lib/stores";
|
||||
import ConfirmationPrompt from "./ConfirmationPrompt.svelte";
|
||||
import { getSavedScenarios, saveScenario, updateScenario, deleteScenario } from "$lib/api/scenarios";
|
||||
|
||||
// Props
|
||||
let {
|
||||
isOpen = $bindable(false),
|
||||
onClose = () => {},
|
||||
onChange = () => {},
|
||||
onSave = () => {},
|
||||
onSelectScenario = (p: SavedScenario) => {},
|
||||
scenario = null,
|
||||
scenario_data = {
|
||||
id: 0,
|
||||
name: "",
|
||||
template_data: {
|
||||
flight_parameters: $FlightParametersStore,
|
||||
description: "",
|
||||
model: "",
|
||||
dataset: "",
|
||||
prediction_mode: "",
|
||||
},
|
||||
},
|
||||
} = $props();
|
||||
|
||||
// Runes
|
||||
let selectedScenario = $derived<SavedScenario | null>(scenario);
|
||||
let isEditing = $state(false);
|
||||
let closeOnSave = $state(false);
|
||||
let isAlertVisible = $state(false);
|
||||
let isConfirmationVisible = $state(false);
|
||||
let alertText = $state("");
|
||||
|
||||
let newScenario = $derived<SavedScenario>(scenario_data as SavedScenario);
|
||||
|
||||
$effect(() => {
|
||||
onChange();
|
||||
});
|
||||
|
||||
// Modal controls
|
||||
export function openModal() {
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
export function openModalAndCreate(
|
||||
scenario: SavedScenario | null = null,
|
||||
scenario_data: SavedScenario = {
|
||||
id: 0,
|
||||
name: "",
|
||||
template_data: {
|
||||
flight_parameters: $FlightParametersStore,
|
||||
description: "",
|
||||
model: "",
|
||||
dataset: "",
|
||||
prediction_mode: "",
|
||||
},
|
||||
},
|
||||
close: boolean = false,
|
||||
onSaveCallback: (point: SavedScenario) => void = () => {},
|
||||
) {
|
||||
if (scenario) {
|
||||
selectedScenario = scenario;
|
||||
newScenario = { ...scenario };
|
||||
isEditing = true;
|
||||
} else {
|
||||
selectedScenario = null;
|
||||
newScenario = scenario_data;
|
||||
isEditing = false;
|
||||
}
|
||||
isOpen = true;
|
||||
closeOnSave = close;
|
||||
onSave = onSaveCallback;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
isOpen = false;
|
||||
closeOnSave = false;
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleDeleteScenario(scenario: SavedScenario | null) {
|
||||
if (!scenario) {
|
||||
return;
|
||||
}
|
||||
deleteScenario(scenario.id)
|
||||
.then(() => {
|
||||
$SavedScenarioStore = $SavedScenarioStore.filter((s) => s.id !== scenario.id);
|
||||
SavedScenarioStore.set($SavedScenarioStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: "Точка удалена",
|
||||
body: `Точка "${scenario.name}" успешно удалена.`,
|
||||
color: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при удалении сценария: ${error.message}`);
|
||||
console.error("Ошибка при удалении сценария:", error);
|
||||
});
|
||||
}
|
||||
|
||||
export function handleSaveScenario() {
|
||||
if (isEditing && selectedScenario) {
|
||||
updateScenario(newScenario)
|
||||
.then((updatedScenario) => {
|
||||
$SavedScenarioStore = $SavedScenarioStore.map((s) =>
|
||||
s.id === updatedScenario.id ? updatedScenario : s,
|
||||
);
|
||||
SavedScenarioStore.set($SavedScenarioStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: "Сценарий обновлен",
|
||||
body: `Сценарий "${updatedScenario.name}" успешно обновлен.`,
|
||||
color: "success",
|
||||
});
|
||||
if (closeOnSave) {
|
||||
closeModal();
|
||||
}
|
||||
onSave(updatedScenario);
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при обновлении сценария: ${error.message}`);
|
||||
});
|
||||
} else {
|
||||
saveScenario(newScenario)
|
||||
.then((savedScenario) => {
|
||||
$SavedScenarioStore = [...$SavedScenarioStore, savedScenario];
|
||||
SavedScenarioStore.set($SavedScenarioStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: "Сценарий сохранен",
|
||||
body: `Сценарий "${savedScenario.name}" успешно сохранен.`,
|
||||
color: "success",
|
||||
});
|
||||
if (closeOnSave) {
|
||||
closeModal();
|
||||
}
|
||||
onSave(savedScenario);
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при сохранении сценария: ${error.message}`);
|
||||
console.error("Ошибка при сохранении сценария:", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function showAlert(message: string) {
|
||||
isAlertVisible = true;
|
||||
alertText = message;
|
||||
}
|
||||
|
||||
export function hideAlert() {
|
||||
isAlertVisible = false;
|
||||
alertText = "";
|
||||
}
|
||||
|
||||
export function resetForm() {
|
||||
hideAlert();
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
{isOpen}
|
||||
toggle={closeModal}
|
||||
size="lg"
|
||||
fade={false}
|
||||
backdrop={true}
|
||||
scrollable
|
||||
class={isConfirmationVisible ? "modal-tinted" : ""}
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Редактирование сценария</h5>
|
||||
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div>
|
||||
<h5>{"Редактирование сценария"}</h5>
|
||||
<Alert
|
||||
color="danger"
|
||||
isOpen={isAlertVisible}
|
||||
toggle={() => (isAlertVisible = false)}
|
||||
fade={false}
|
||||
class="mb-2"
|
||||
>
|
||||
<Icon name="exclamation-triangle" class="me-2" />
|
||||
{alertText}
|
||||
</Alert>
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSaveScenario();
|
||||
}}
|
||||
>
|
||||
<div class="mb-2">
|
||||
<Label for="name" class="small">Название сценария:</Label>
|
||||
<Input class="form-control-sm" type="text" id="name" bind:value={newScenario.name} required />
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<Button type="submit" color="success" size="sm">
|
||||
{isEditing ? "Обновить сценарий" : "Сохранить сценарий"}
|
||||
</Button>
|
||||
{#if isEditing}
|
||||
<Button size="sm" type="button" color="secondary" onclick={resetForm}>Отмена</Button>
|
||||
{/if}
|
||||
<span class="flex-grow-1"></span>
|
||||
{#if isEditing}
|
||||
<Button color="danger" size="sm" type="button" onclick={() => {}}>
|
||||
Удалить сценарий
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onclick={() => {
|
||||
resetForm();
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
Закрыть без сохранения
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmationPrompt
|
||||
isOpen={isConfirmationVisible}
|
||||
title="Подтвердите удаление"
|
||||
confirmText="Удалить"
|
||||
cancelText="Отмена"
|
||||
confirmVariant="danger"
|
||||
onconfirm={() => {
|
||||
isConfirmationVisible = false;
|
||||
handleDeleteScenario(selectedScenario);
|
||||
}}
|
||||
oncancel={() => {
|
||||
isConfirmationVisible = false;
|
||||
}}
|
||||
>
|
||||
<p>Вы уверены, что хотите удалить этот сценарий?</p>
|
||||
</ConfirmationPrompt>
|
||||
|
|
@ -12,11 +12,131 @@
|
|||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { PROFILE_MAP } from "$lib/types";
|
||||
import { FlightParametersStore, writeLocalStorage, ScenarioStore, SavedScenarioTemplatesStore } from "$lib/stores";
|
||||
import { PREDICTION_MODE_MAP, PPREDICTION_MODE_NAMES } from "$lib/types";
|
||||
import type { SavedScenario } from "$lib/types";
|
||||
import { getSavedScenarios, updateScenario, saveScenario } from "$lib/api/scenarios";
|
||||
import { FlightParametersStore, writeLocalStorage, ScenarioStore, SavedScenarioStore } from "$lib/stores";
|
||||
import SelectSearchable from "$lib/components/SelectSearchable.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { addToast } from "./Toast.svelte";
|
||||
import ScenarioEditor from "./ScenarioEditor.svelte";
|
||||
|
||||
let isCollapsed = false;
|
||||
let isCollapsed = $state(false);
|
||||
let scenarioUnsaved = $derived(checkScenarioUnsaved());
|
||||
let selectedScenarioId = $state(-1);
|
||||
|
||||
let scenarioEditorRef: ScenarioEditor | null = null;
|
||||
|
||||
onMount(() => {
|
||||
getSavedScenarios()
|
||||
.then((scenarios) => SavedScenarioStore.set(scenarios))
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Error Loading Points",
|
||||
body: `Failed to load saved points: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
return [];
|
||||
});
|
||||
selectedScenarioId = $ScenarioStore.id;
|
||||
});
|
||||
|
||||
function checkScenarioUnsaved() {
|
||||
const savedScenario = $SavedScenarioStore.find((scenario) => scenario.id === $ScenarioStore.id);
|
||||
|
||||
if (!savedScenario) {
|
||||
return false; // No saved scenario found
|
||||
}
|
||||
|
||||
const flightParameters = $FlightParametersStore;
|
||||
|
||||
const savedData = savedScenario.template_data;
|
||||
const savedFlightParameters = savedData.flight_parameters;
|
||||
|
||||
// Compare flight parameters excluding launch_datetime
|
||||
return JSON.stringify(flightParameters) !== JSON.stringify(savedFlightParameters);
|
||||
}
|
||||
|
||||
function handleSaveCurrentScenario() {
|
||||
console.log("handleSaveCurrentScenario called");
|
||||
const scenario = $SavedScenarioStore.find((s) => s.id === selectedScenarioId);
|
||||
if (selectedScenarioId !== -1 && scenario) {
|
||||
$ScenarioStore.id = selectedScenarioId;
|
||||
updateScenario($ScenarioStore)
|
||||
.then((updatedScenario) => {
|
||||
$SavedScenarioStore = $SavedScenarioStore.map((s) =>
|
||||
s.id === updatedScenario.id ? updatedScenario : s,
|
||||
);
|
||||
SavedScenarioStore.set($SavedScenarioStore);
|
||||
$ScenarioStore = updatedScenario;
|
||||
selectedScenarioId = updatedScenario.id;
|
||||
addToast({
|
||||
header: "Сценарий обновлен",
|
||||
body: `Сценарий "${updatedScenario.name}" успешно обновлен.`,
|
||||
color: "success",
|
||||
});
|
||||
scenarioUnsaved = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Ошибка обновления сценария",
|
||||
body: `Ошибка при обновлении сценария: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
console.error("Ошибка при обновлении сценария:", error);
|
||||
});
|
||||
} else {
|
||||
scenarioEditorRef?.openModalAndCreate(
|
||||
null,
|
||||
{
|
||||
id: 0,
|
||||
name: "",
|
||||
template_data: {
|
||||
flight_parameters: $FlightParametersStore,
|
||||
description: "test",
|
||||
model: "test",
|
||||
dataset: "test",
|
||||
prediction_mode: $ScenarioStore.template_data.prediction_mode
|
||||
},
|
||||
},
|
||||
true,
|
||||
handleModalSave,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleApplySelectedScenario(showToast = true) {
|
||||
const selectedScenario = $SavedScenarioStore.find((scenario) => scenario.id === selectedScenarioId);
|
||||
if (selectedScenario) {
|
||||
$ScenarioStore = selectedScenario;
|
||||
$FlightParametersStore = selectedScenario.template_data.flight_parameters;
|
||||
scenarioUnsaved = false;
|
||||
writeLocalStorage("scenario", $ScenarioStore);
|
||||
if (showToast) {
|
||||
addToast({
|
||||
header: "Сценарий применен",
|
||||
body: `Сценарий "${selectedScenario.name}" успешно применен.`,
|
||||
color: "success",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (showToast)
|
||||
addToast({
|
||||
header: "Сценарий не найден",
|
||||
body: "Выбранный сценарий не существует.",
|
||||
color: "warning",
|
||||
});
|
||||
console.warn("Selected scenario not found:", selectedScenarioId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalSave(savedScenario: SavedScenario) {
|
||||
if (savedScenario) {
|
||||
$ScenarioStore = savedScenario;
|
||||
selectedScenarioId = savedScenario.id;
|
||||
scenarioUnsaved = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const collapsePanel = () => {
|
||||
isCollapsed = true;
|
||||
|
|
@ -41,10 +161,10 @@
|
|||
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
||||
style="width:100%;"
|
||||
aria-label="Свернуть/развернуть параметры прогнозирования"
|
||||
on:click={() => (isCollapsed = !isCollapsed)}
|
||||
onclick={() => (isCollapsed = !isCollapsed)}
|
||||
>
|
||||
<b class="card-title mb-0 text-white p-0">Сценарий прогнозирования</b>
|
||||
<Button class="p-0" size="sm" color="primary" on:click={() => (isCollapsed = !isCollapsed)}>
|
||||
<Button class="p-0" size="sm" color="primary" onclick={() => (isCollapsed = !isCollapsed)}>
|
||||
{#if isCollapsed}
|
||||
<Icon name="caret-left-fill" class="text-white" />
|
||||
{:else}
|
||||
|
|
@ -58,50 +178,77 @@
|
|||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioName" class="form-label">Cценарий:</Label>
|
||||
<InputGroup size="sm">
|
||||
<div class="position-relative flex-grow-1">
|
||||
<SelectSearchable
|
||||
id="startPoint"
|
||||
options={$SavedScenarioTemplatesStore.map(scenario => ({
|
||||
style="min-height: calc(1.5em + .5rem + 2px); padding: .25rem .5rem; font-size: .875rem;"
|
||||
id="cp-start-point"
|
||||
options={$SavedScenarioStore.map((scenario) => ({
|
||||
value: scenario.id,
|
||||
label: scenario.name,
|
||||
label:
|
||||
scenario.name +
|
||||
`${scenario.id == $ScenarioStore.id && scenarioUnsaved ? " (изменено)" : ""}`,
|
||||
}))}
|
||||
placeholder="Выберите сценарий..."
|
||||
bind:selected={selectedScenarioId}
|
||||
placeholder="Новый сценарий..."
|
||||
searchPlaceholder="Поиск сценариев..."
|
||||
on:change={() => {
|
||||
if (!scenarioUnsaved) {
|
||||
handleApplySelectedScenario(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button color="success" title="Применить сценарий">
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white); z-index: 10; margin-right: 2rem;"
|
||||
on:click={() => {
|
||||
selectedScenarioId = -1;
|
||||
}}
|
||||
disabled={selectedScenarioId === -1}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button color="success" title="Применить сценарий" onclick={() => {handleApplySelectedScenario(true)}}>
|
||||
<span>✓</span>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<Button class="flex-fill" color="secondary" size="sm">
|
||||
Сохранить
|
||||
<Icon name="save" />
|
||||
</Button>
|
||||
<Button class="flex-fill" color="secondary" size="sm">
|
||||
Загрузить
|
||||
<Icon name="folder2-open" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
class="mb-0 w-100"
|
||||
>
|
||||
Редактировать сохраненные сценарии
|
||||
<Button color="secondary flex-fill" size="sm" title="Открыть список сценариев">
|
||||
Все сценарии
|
||||
<Icon name="journal-bookmark-fill" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary flex-fill"
|
||||
size="sm"
|
||||
title="Сохранить текущие условия как сценарий"
|
||||
onclick={handleSaveCurrentScenario}
|
||||
disabled={!scenarioUnsaved && selectedScenarioId !== -1}
|
||||
>
|
||||
{selectedScenarioId !== -1 ? "Обновить сценарий" : "Сохранить сценарий"}
|
||||
<Icon name="floppy2-fill" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Режим сценария:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="scenarioMode">
|
||||
<option>Обычный</option>
|
||||
<option>Почасовой</option>
|
||||
<option>Ансамблевый</option>
|
||||
<Input type="select" id="scenarioMode" bind:value={$ScenarioStore.template_data.prediction_mode}
|
||||
on:change={() => {
|
||||
scenarioUnsaved = true;
|
||||
}}>
|
||||
{#each Object.entries(PREDICTION_MODE_MAP) as [key, value]}
|
||||
<option {value}
|
||||
>{PPREDICTION_MODE_NAMES[key as keyof typeof PPREDICTION_MODE_NAMES]}
|
||||
{key}
|
||||
</option>
|
||||
{/each}
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
|
@ -141,7 +288,7 @@
|
|||
<Button
|
||||
color="primary"
|
||||
title="Edit Saved Locations"
|
||||
on:click={() => console.log("Not implemented yet")}
|
||||
onclick={() => console.log("Not implemented yet")}
|
||||
>
|
||||
<span>Экспорт</span>
|
||||
<Icon name="file-earmark-arrow-down" />
|
||||
|
|
@ -151,3 +298,4 @@
|
|||
</CardBody>
|
||||
{/if}
|
||||
</Card>
|
||||
<ScenarioEditor bind:this={scenarioEditorRef} onSave={handleModalSave} />
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
searchPlaceholder?: string;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
onChange?: (value: any) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -74,6 +75,9 @@
|
|||
isOpen = false;
|
||||
searchTerm = '';
|
||||
dispatch('change', selected);
|
||||
if (restProps.onChange) {
|
||||
restProps.onChange(selected);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
|
|
@ -129,7 +133,7 @@
|
|||
{#each filteredOptions as option}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item "
|
||||
class="dropdown-item small"
|
||||
class:active={option.value === selected}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -46,9 +46,11 @@ function formatLaunchDateTime(dateObj: string | Date, timeStr: string): string {
|
|||
|
||||
export const getForecast = async (
|
||||
flightParameters: Record<string, any>,
|
||||
launchDateTime: string
|
||||
): Promise<void> => {
|
||||
// Create request object
|
||||
flightParameters.dataset = getLatestDataset();
|
||||
flightParameters.launch_datetime = launchDateTime;
|
||||
|
||||
console.log("Sending request:", flightParameters);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { writable } from "svelte/store";
|
||||
import type { FlightParameters, RawTelemetry, Telemetry } from "./types";
|
||||
import type { RawPrediction, Prediction } from "./types";
|
||||
import type { SavedPoint, SavedFlightProfile, SavedScenarioTemplate, TemplateData } from "./types";
|
||||
import type { SavedPoint, SavedFlightProfile, SavedScenario, TemplateData } from "./types";
|
||||
|
||||
export const readLocalStorage = <T>(key: string, defaultValue: T): T => {
|
||||
const item = localStorage.getItem(key);
|
||||
|
|
@ -42,7 +42,6 @@ const flightParametersDefaults: FlightParameters = {
|
|||
descent_rate: 5.0,
|
||||
format: "json",
|
||||
launch_altitude: 0.0,
|
||||
launch_datetime: "",
|
||||
launch_latitude: 62.1234,
|
||||
launch_longitude: 129.1234,
|
||||
profile: "standard_profile",
|
||||
|
|
@ -55,14 +54,14 @@ export const FlightParametersStore = writable<FlightParameters>(
|
|||
|
||||
const templateDataDefaults: TemplateData = {
|
||||
description: "",
|
||||
prediction_mode: false,
|
||||
prediction_mode: "",
|
||||
model: "",
|
||||
dataset: "",
|
||||
flight_parameters: flightParametersDefaults,
|
||||
};
|
||||
|
||||
export const ScenarioStore = writable<TemplateData>(
|
||||
readLocalStorage<TemplateData>("scenario", templateDataDefaults)
|
||||
export const ScenarioStore = writable<SavedScenario>(
|
||||
readLocalStorage<SavedScenario>("scenario", {} as SavedScenario)
|
||||
);
|
||||
|
||||
export const RawTelemetryStore = writable<RawTelemetry>(
|
||||
|
|
@ -87,4 +86,4 @@ export const SavedPointsStore = writable<SavedPoint[]>([]);
|
|||
export const SavedFlightProfilesStore = writable<SavedFlightProfile[]>([]);
|
||||
|
||||
// stub
|
||||
export const SavedScenarioTemplatesStore = writable<SavedScenarioTemplate[]>([]);
|
||||
export const SavedScenarioStore = writable<SavedScenario[]>([]);
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import type { LatLngExpression, LatLngLiteral } from "leaflet";
|
||||
|
||||
export const PROFILE_MAP = {
|
||||
Normal: "standard_profile",
|
||||
Float: "float_profile",
|
||||
Reverse: "reverse_profile",
|
||||
Custom: "custom_profile",
|
||||
"Обычный": "standard_profile",
|
||||
"Дрейф": "float_profile",
|
||||
"Реверсивный": "reverse_profile",
|
||||
"Пользовательский": "custom_profile",
|
||||
};
|
||||
|
||||
// Map of profile names to their string identifiers
|
||||
export const PROFILE_NAMES = {
|
||||
standard_profile: "Normal",
|
||||
float_profile: "Float",
|
||||
reverse_profile: "Reverse",
|
||||
custom_profile: "Custom",
|
||||
standard_profile: "Обычный",
|
||||
float_profile: "Дрейф",
|
||||
reverse_profile: "Реверсивный",
|
||||
custom_profile: "Пользовательский"
|
||||
};
|
||||
|
||||
export type ProfileName = keyof typeof PROFILE_MAP;
|
||||
|
|
@ -26,7 +26,6 @@ export interface FlightParameters {
|
|||
descent_rate: number;
|
||||
format: "json";
|
||||
launch_altitude: number;
|
||||
launch_datetime: string;
|
||||
launch_latitude: number;
|
||||
launch_longitude: number;
|
||||
profile: (typeof PROFILE_MAP)[ProfileName];
|
||||
|
|
@ -36,9 +35,22 @@ export interface FlightParameters {
|
|||
template?: number; // Optional, used for saved scenarios
|
||||
}
|
||||
|
||||
export const PREDICTION_MODE_MAP = {
|
||||
"Разовый": "single",
|
||||
"Почасовой": "hourly",
|
||||
"Ансамблевый": "ensemble"
|
||||
};
|
||||
|
||||
// Map of profile names to their string identifiers
|
||||
export const PPREDICTION_MODE_NAMES = {
|
||||
single: "Разовый",
|
||||
hourly: "Почасовой",
|
||||
ensemble: "Ансамблевый"
|
||||
};
|
||||
|
||||
export interface TemplateData {
|
||||
description: string;
|
||||
prediction_mode: boolean;
|
||||
prediction_mode: string;
|
||||
model: string;
|
||||
dataset: string;
|
||||
flight_parameters: FlightParameters;
|
||||
|
|
@ -120,7 +132,7 @@ export interface SavedFlightProfile {
|
|||
rate_profile_data: object;
|
||||
}
|
||||
|
||||
export interface SavedScenarioTemplate {
|
||||
export interface SavedScenario {
|
||||
id: number;
|
||||
name: string;
|
||||
template_data: TemplateData;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import PanelContainer from "$lib/components/PanelContainer.svelte";
|
||||
import ScenarioPanel from "$lib/components/ScenarioPanel.svelte";
|
||||
import TabComponent from "$lib/components/TabComponent.svelte";
|
||||
import PointListModal from "$lib/components/PointListModal.svelte";
|
||||
import PointEditor from "$lib/components/PointEditor.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { PredictionStore } from "$lib/stores";
|
||||
import { addToast, removeToast } from "$lib/components/Toast.svelte";
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
|
||||
<div>
|
||||
{#if activeTab === 'control'}
|
||||
<ControlPanel {handleClickSelectOnMap} bind:this={controlPanel} />
|
||||
<ControlPanel onSelectOnMapClick={handleClickSelectOnMap} bind:this={controlPanel} />
|
||||
{:else if activeTab === 'scenario'}
|
||||
<ScenarioPanel />
|
||||
{:else if activeTab === 'settings'}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@
|
|||
import { addToast } from "$lib/components/Toast.svelte";
|
||||
|
||||
// TODO: Implement these imports
|
||||
import { SavedPointsStore, SavedFlightProfilesStore, SavedScenarioTemplatesStore } from "$lib/stores";
|
||||
import { SavedPointsStore, SavedFlightProfilesStore, SavedScenarioStore } from "$lib/stores";
|
||||
import { getSavedPoints, deletePoint } from "$lib/api/points";
|
||||
import { getSavedFlightProfiles, deleteFlightProfile } from "$lib/api/profiles";
|
||||
import { getSavedScenarioTemplates, deleteScenarioTemplate } from "$lib/api/templates";
|
||||
import type { SavedPoint, SavedFlightProfile, SavedScenarioTemplate } from "$lib/types";
|
||||
import { getSavedScenarios, deleteScenario } from "$lib/api/scenarios";
|
||||
import type { SavedPoint, SavedFlightProfile, SavedScenario } from "$lib/types";
|
||||
|
||||
// Table handlers
|
||||
let pointsTable = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 5 }));
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
let profilesTable = $derived(new TableHandler($SavedFlightProfilesStore, { rowsPerPage: 5 }));
|
||||
let profilesSearch = $derived(profilesTable.createSearch(["name"]));
|
||||
|
||||
let templatesTable = $derived(new TableHandler($SavedScenarioTemplatesStore, { rowsPerPage: 5 }));
|
||||
let templatesTable = $derived(new TableHandler($SavedScenarioStore, { rowsPerPage: 5 }));
|
||||
let templatesSearch = $derived(templatesTable.createSearch(["name"]));
|
||||
|
||||
let editPoint: SavedPoint | null = $state(null);
|
||||
|
|
@ -49,10 +49,7 @@
|
|||
{ id: 1, name: "Standard Weather Balloon", rate_profile_data: {ascent_rate: 5, descent_rate: 8, burst_altitude: 30000} },
|
||||
{ id: 2, name: "High Altitude Probe", rate_profile_data: {ascent_rate: 6, descent_rate: 10, burst_altitude: 40000} },
|
||||
];
|
||||
$SavedScenarioTemplatesStore = [
|
||||
{ id: 1, name: "Summer Launch from Baikonur", template_data: {description: "Standard summer conditions test."} },
|
||||
{ id: 2, name: "Winter Launch from KSC", template_data: {description: "High wind scenario."} },
|
||||
];
|
||||
|
||||
|
||||
/*
|
||||
// TODO: Uncomment when API is ready
|
||||
|
|
@ -374,6 +371,9 @@
|
|||
point={editPoint}
|
||||
isOpen={editPoint !== null}
|
||||
onClose={() => { editPoint = null; pointsTable.setRows($SavedPointsStore) }}
|
||||
editor={true}
|
||||
closeOnSave={true}
|
||||
closeOnDelete={true}
|
||||
/>
|
||||
|
||||
<ToastContainer />
|
||||
Loading…
Add table
Add a link
Reference in a new issue