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">
|
<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 {
|
import {
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardBody,
|
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
Label,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupText,
|
InputGroupText,
|
||||||
Icon,
|
Label,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} 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 SelectSearchable from "$lib/components/SelectSearchable.svelte";
|
||||||
import { getForecast } from "$lib/prediction";
|
import { getForecast } from "$lib/prediction";
|
||||||
import type { FlightParameters, ProfileName, ProfileIdentifier, SavedPoint } from "$lib/types";
|
import { FlightParametersStore, SavedPointsStore, writeLocalStorage } from "$lib/stores";
|
||||||
import { PROFILE_MAP, PROFILE_NAMES } from "$lib/types";
|
import { PROFILE_MAP, type FlightParameters, type ProfileName, type SavedPoint } 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;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
handleClickSelectOnMap?: () => void;
|
onSelectOnMapClick?: () => void;
|
||||||
}
|
}
|
||||||
|
let { onSelectOnMapClick = () => console.log("Select on map clicked") }: Props = $props();
|
||||||
let {
|
|
||||||
handleClickSelectOnMap = () => console.log("Select on map clicked"),
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let isCollapsed = $state(false);
|
let isCollapsed = $state(false);
|
||||||
|
let isPointDirty = $state(false);
|
||||||
let pointListModal: PointListModal | null = null;
|
|
||||||
|
|
||||||
// Initialize date/time
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let startDate = $state(now.toISOString().split("T")[0]);
|
let startDate = $state(now.toISOString().split("T")[0]);
|
||||||
let startTime = $state(now.toISOString().split("T")[1].split(".")[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
|
// Component References
|
||||||
let inputLat = $derived($FlightParametersStore.start_point === -1
|
let PointEditorRef: PointEditor | null = null;
|
||||||
|
|
||||||
|
// Derived State
|
||||||
|
let inputLat = $derived(
|
||||||
|
$FlightParametersStore.start_point === -1
|
||||||
? $FlightParametersStore.launch_latitude.toFixed(6)
|
? $FlightParametersStore.launch_latitude.toFixed(6)
|
||||||
: $SavedPointsStore.find(point => point.id === $FlightParametersStore.start_point)?.lat.toFixed(6) || "0.000000");
|
: $SavedPointsStore.find((point) => point.id === $FlightParametersStore.start_point)?.lat.toFixed(6) ||
|
||||||
let inputLng = $derived($FlightParametersStore.start_point === -1
|
"0.000000",
|
||||||
|
);
|
||||||
|
let inputLng = $derived(
|
||||||
|
$FlightParametersStore.start_point === -1
|
||||||
? $FlightParametersStore.launch_longitude.toFixed(6)
|
? $FlightParametersStore.launch_longitude.toFixed(6)
|
||||||
: $SavedPointsStore.find(point => point.id === $FlightParametersStore.start_point)?.lon.toFixed(6) || "0.000000");
|
: $SavedPointsStore.find((point) => point.id === $FlightParametersStore.start_point)?.lon.toFixed(6) ||
|
||||||
let inputAlt = $derived($FlightParametersStore.start_point === -1
|
"0.000000",
|
||||||
|
);
|
||||||
|
let inputAlt = $derived(
|
||||||
|
$FlightParametersStore.start_point === -1
|
||||||
? $FlightParametersStore.launch_altitude.toFixed(2)
|
? $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() {
|
// Lifecycle Hooks
|
||||||
if ($FlightParametersStore.start_point !== -1) {
|
onMount(() => {
|
||||||
$FlightParametersStore.start_point = -1;
|
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() {
|
export function handleSelectPointInModal(point: SavedPoint) {
|
||||||
if (selectedPoint && selectedPoint !== -1) {
|
console.log("Selected point from modal:", point);
|
||||||
$FlightParametersStore.start_point = selectedPoint;
|
selectedPointId = point.id;
|
||||||
}
|
$FlightParametersStore.start_point = selectedPointId;
|
||||||
|
isPointDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCurrentPoint() {
|
function handleSaveCurrentPoint() {
|
||||||
if (selectedPoint !== -1) {
|
if (selectedPointId !== -1) {
|
||||||
const point = $SavedPointsStore.find(p => p.id === selectedPoint);
|
const point = $SavedPointsStore.find((p) => p.id === selectedPointId);
|
||||||
if (point) {
|
if (point) {
|
||||||
|
point.lat = parseFloat(inputLat);
|
||||||
|
point.lon = parseFloat(inputLng);
|
||||||
|
point.alt = parseFloat(inputAlt);
|
||||||
updatePoint(point)
|
updatePoint(point)
|
||||||
.then((updatedPoint) => {
|
.then((updatedPoint) => {
|
||||||
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||||
|
|
@ -108,6 +138,7 @@
|
||||||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||||
color: "success",
|
color: "success",
|
||||||
});
|
});
|
||||||
|
isPointDirty = false;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
addToast({
|
addToast({
|
||||||
|
|
@ -119,81 +150,67 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pointListModal?.openModalAndCreate(null, {
|
PointEditorRef?.openModalAndCreate(
|
||||||
|
null,
|
||||||
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: `Новая точка ${new Date().toLocaleString()}`,
|
name: `Новая точка ${new Date().toLocaleString()}`,
|
||||||
lat: parseFloat(inputLat),
|
lat: parseFloat(inputLat),
|
||||||
lon: parseFloat(inputLng),
|
lon: parseFloat(inputLng),
|
||||||
alt: parseFloat(inputAlt),
|
alt: parseFloat(inputAlt),
|
||||||
}, true, onModalSave);
|
},
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
handleModalSave,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onModalSave(savedPoint: SavedPoint) {
|
function handleModalSave(savedPoint: SavedPoint) {
|
||||||
if (savedPoint) {
|
if (savedPoint) {
|
||||||
$FlightParametersStore.start_point = savedPoint.id;
|
$FlightParametersStore.start_point = savedPoint.id;
|
||||||
selectedPoint = savedPoint.id;
|
selectedPointId = savedPoint.id;
|
||||||
setToCustomOnChange();
|
isPointDirty = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickPointListModal() {
|
function handlePointEditorOpen() {
|
||||||
pointListModal?.openModal();
|
PointEditorRef?.openModal(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// function applyCoordinatesFromInput() {
|
async function handlePredictionRequest() {
|
||||||
// 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`;
|
|
||||||
$FlightParametersStore.launch_latitude = parseFloat(inputLat);
|
$FlightParametersStore.launch_latitude = parseFloat(inputLat);
|
||||||
$FlightParametersStore.launch_longitude = parseFloat(inputLng);
|
$FlightParametersStore.launch_longitude = parseFloat(inputLng);
|
||||||
$FlightParametersStore.launch_altitude = parseFloat(inputAlt);
|
$FlightParametersStore.launch_altitude = parseFloat(inputAlt);
|
||||||
writeLocalStorage<FlightParameters>("flightParameters", $FlightParametersStore);
|
writeLocalStorage<FlightParameters>("flightParameters", $FlightParametersStore);
|
||||||
|
|
||||||
getForecast($FlightParametersStore)
|
try {
|
||||||
.then((data) => {
|
const data = await getForecast($FlightParametersStore, `${startDate}T${startTime}Z`);
|
||||||
console.log("Forecast request successful:", data);
|
console.log("Forecast request successful:", data);
|
||||||
addToast({
|
addToast({
|
||||||
header: "Forecast Request",
|
header: "Forecast Request",
|
||||||
body: "Forecast request successful!",
|
body: "Forecast request successful!",
|
||||||
color: "success",
|
color: "success",
|
||||||
});
|
});
|
||||||
// Handle the response data as needed
|
} catch (error: any) {
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error getting forecast:", error);
|
console.error("Error getting forecast:", error);
|
||||||
addToast({
|
addToast({
|
||||||
header: "Forecast Error",
|
header: "Forecast Error",
|
||||||
body: `Error getting forecast: ${error.message}`,
|
body: `Error getting forecast: ${error.message}`,
|
||||||
color: "danger",
|
color: "danger",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleCollapse() {
|
function handleToggleCollapse() {
|
||||||
isCollapsed = !isCollapsed;
|
isCollapsed = !isCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exported functions for parent components
|
// Public API
|
||||||
export function updateLaunchPosition(lat: number, lng: number) {
|
export function updateLaunchPosition(lat: number, lng: number) {
|
||||||
$FlightParametersStore.launch_latitude = lat;
|
$FlightParametersStore.launch_latitude = lat;
|
||||||
$FlightParametersStore.launch_longitude = lng;
|
$FlightParametersStore.launch_longitude = lng;
|
||||||
inputLat = lat.toFixed(6);
|
isPointDirty = true;
|
||||||
inputLng = lng.toFixed(6);
|
|
||||||
setToCustomOnChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedProfile() {
|
export function getSelectedProfile() {
|
||||||
|
|
@ -215,21 +232,6 @@
|
||||||
export function togglePanel() {
|
export function togglePanel() {
|
||||||
isCollapsed = !isCollapsed;
|
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>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -240,11 +242,11 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
||||||
aria-label="Свернуть/развернуть параметры прогнозирования"
|
aria-label="Свернуть/развернуть условия прогнозирования"
|
||||||
onclick={toggleCollapse}
|
onclick={handleToggleCollapse}
|
||||||
>
|
>
|
||||||
<b class="card-title mb-0 text-white p-0">Параметры прогнозирования</b>
|
<b class="card-title mb-0 text-white p-0">Условия прогнозирования</b>
|
||||||
<Button class="p-0" size="sm" color="primary" onclick={toggleCollapse}>
|
<Button class="p-0" size="sm" color="primary" onclick={handleToggleCollapse}>
|
||||||
{#if isCollapsed}
|
{#if isCollapsed}
|
||||||
<Icon name="caret-left-fill" class="text-white" />
|
<Icon name="caret-left-fill" class="text-white" />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -258,28 +260,181 @@
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||||
<Label for="startTime" class="form-label">Время старта (UTC):</Label>
|
<Label for="cp-start-time" class="form-label">Время старта (UTC):</Label>
|
||||||
<Input type="time" id="startTime" class="form-control-sm" bind:value={startTime} step="1" />
|
<Input type="time" id="cp-start-time" class="form-control-sm" bind:value={startTime} step="1" />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||||
<Label for="startDate" class="form-label">Дата старта:</Label>
|
<Label for="cp-start-date" class="form-label">Дата старта:</Label>
|
||||||
<Input type="date" id="startDate" class="form-control-sm" bind:value={startDate} />
|
<Input type="date" id="cp-start-date" class="form-control-sm" bind:value={startDate} />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormGroup spacing="mb-2">
|
<FormGroup spacing="mb-2">
|
||||||
<Label for="flightProfile" class="form-label">Профиль полета:</Label>
|
<Label for="cp-flight-profile" class="form-label">Профиль полета:</Label>
|
||||||
<InputGroup size="sm">
|
<InputGroup size="sm">
|
||||||
<Input type="select" id="flightProfile" bind:value={$FlightParametersStore.profile}>
|
<Input type="select" id="cp-flight-profile" bind:value={$FlightParametersStore.profile}>
|
||||||
<optgroup label="Стандартные профили">
|
|
||||||
{#each Object.keys(PROFILE_MAP) as profileName}
|
{#each Object.keys(PROFILE_MAP) as profileName}
|
||||||
<option value={PROFILE_MAP[profileName as ProfileName]}>{profileName}</option>
|
<option value={PROFILE_MAP[profileName as ProfileName]}>{profileName}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</optgroup>
|
|
||||||
<optgroup label="Пользовательские профили">
|
|
||||||
<option>Custom</option>
|
|
||||||
</optgroup>
|
|
||||||
</Input>
|
</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
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -291,138 +446,12 @@
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</FormGroup>
|
</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}
|
{/if}
|
||||||
|
|
||||||
<div class="d-grid gap-1">
|
<div class="d-grid gap-1">
|
||||||
<Button color="outline-secondary" size="sm">Показать график высоты</Button>
|
<Button size="sm" color="primary" onclick={handlePredictionRequest}>Выполнить прогнозирование</Button>
|
||||||
<Button size="sm" color="primary" onclick={handleGetPrediction}>Выполнить прогнозирование</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
{/if}
|
{/if}
|
||||||
</Card>
|
</Card>
|
||||||
<PointListModal bind:this={pointListModal} />
|
<PointEditor bind:this={PointEditorRef} onSelectPoint={handleSelectPointInModal} />
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationItem,
|
PaginationItem,
|
||||||
PaginationLink,
|
PaginationLink,
|
||||||
|
InputGroup,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { addToast } from "$lib/components/Toast.svelte";
|
import { addToast } from "$lib/components/Toast.svelte";
|
||||||
|
|
@ -20,39 +21,116 @@
|
||||||
import { getSavedPoints, savePoint, updatePoint, deletePoint } from "$lib/api/points";
|
import { getSavedPoints, savePoint, updatePoint, deletePoint } from "$lib/api/points";
|
||||||
|
|
||||||
// Props
|
// 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
|
// 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 isAlertVisible = $state(false);
|
||||||
let isConfirmationVisible = $state(false);
|
let isConfirmationVisible = $state(false);
|
||||||
let alertText = $state("");
|
let alertText = $state("");
|
||||||
|
let closeOnSave_ = $state(closeOnSave);
|
||||||
|
|
||||||
|
// Table handler
|
||||||
|
let table = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 10 }));
|
||||||
|
let search = $derived(table.createSearch(["name"]));
|
||||||
|
|
||||||
$effect(() => {
|
$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
|
// Modal controls
|
||||||
export function openModal() {
|
export function openModal(table_: boolean = false) {
|
||||||
|
showTable = table_;
|
||||||
isOpen = true;
|
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() {
|
function closeModal() {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
|
if (closeOnSave_ != closeOnSave) {
|
||||||
|
closeOnSave = closeOnSave_;
|
||||||
|
}
|
||||||
onClose();
|
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)
|
deletePoint(point.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
$SavedPointsStore = $SavedPointsStore.filter((p) => p.id !== point.id);
|
$SavedPointsStore = $SavedPointsStore.filter((p) => p.id !== point.id);
|
||||||
SavedPointsStore.set($SavedPointsStore);
|
SavedPointsStore.set($SavedPointsStore);
|
||||||
resetForm();
|
|
||||||
addToast({
|
addToast({
|
||||||
header: "Точка удалена",
|
header: "Точка удалена",
|
||||||
body: `Точка "${point.name}" успешно удалена.`,
|
body: `Точка "${point.name}" успешно удалена.`,
|
||||||
color: "success",
|
color: "success",
|
||||||
});
|
});
|
||||||
|
if (closeOnDelete) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
showAlert(`Ошибка при удалении точки: ${error.message}`);
|
showAlert(`Ошибка при удалении точки: ${error.message}`);
|
||||||
|
|
@ -60,8 +138,9 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSavePoint() {
|
export function handleSavePoint() {
|
||||||
updatePoint(selectedPoint)
|
if (isEditing && selectedPoint) {
|
||||||
|
updatePoint(newPoint)
|
||||||
.then((updatedPoint) => {
|
.then((updatedPoint) => {
|
||||||
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||||
SavedPointsStore.set($SavedPointsStore);
|
SavedPointsStore.set($SavedPointsStore);
|
||||||
|
|
@ -71,10 +150,35 @@
|
||||||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||||
color: "success",
|
color: "success",
|
||||||
});
|
});
|
||||||
|
if (closeOnSave_) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
onSave(updatedPoint);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
showAlert(`Ошибка при обновлении точки: ${error.message}`);
|
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) {
|
export function showAlert(message: string) {
|
||||||
|
|
@ -88,19 +192,111 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetForm() {
|
export function resetForm() {
|
||||||
|
selectedPoint = null;
|
||||||
|
newPoint = { id: 0, name: "", lat: 0, lon: 0, alt: 0 };
|
||||||
|
isEditing = false;
|
||||||
hideAlert();
|
hideAlert();
|
||||||
closeModal();
|
|
||||||
}
|
}
|
||||||
</script>
|
</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">
|
<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>
|
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<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>
|
<div>
|
||||||
<h5>{"Редактирование точки"}</h5>
|
{#if showTable}
|
||||||
|
<h5>{isEditing ? "Редактирование точки" : "Добавить новую точку"}</h5>
|
||||||
|
{/if}
|
||||||
<Alert
|
<Alert
|
||||||
color="danger"
|
color="danger"
|
||||||
isOpen={isAlertVisible}
|
isOpen={isAlertVisible}
|
||||||
|
|
@ -119,7 +315,7 @@
|
||||||
>
|
>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<Label for="name" class="small">Название точки:</Label>
|
<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>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<FormGroup class="flex-grow-1">
|
<FormGroup class="flex-grow-1">
|
||||||
|
|
@ -129,7 +325,7 @@
|
||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
id="lat"
|
id="lat"
|
||||||
bind:value={selectedPoint.lat}
|
bind:value={newPoint.lat}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<span class="form-text">Градусы</span>
|
<span class="form-text">Градусы</span>
|
||||||
|
|
@ -141,7 +337,7 @@
|
||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
id="lon"
|
id="lon"
|
||||||
bind:value={selectedPoint.lon}
|
bind:value={newPoint.lon}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<span class="form-text">Градусы</span>
|
<span class="form-text">Градусы</span>
|
||||||
|
|
@ -153,7 +349,7 @@
|
||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
id="alt"
|
id="alt"
|
||||||
bind:value={selectedPoint.alt}
|
bind:value={newPoint.alt}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<span class="form-text">Метры над ур. моря</span>
|
<span class="form-text">Метры над ур. моря</span>
|
||||||
|
|
@ -161,18 +357,29 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid gap-2 d-md-flex">
|
<div class="d-grid gap-2 d-md-flex">
|
||||||
<Button type="submit" color="success" size="sm">
|
<Button type="submit" color="success" size="sm">
|
||||||
Обновить точку
|
{isEditing ? "Обновить точку" : "Сохранить точку"}
|
||||||
</Button>
|
</Button>
|
||||||
|
{#if isEditing}
|
||||||
<Button size="sm" type="button" color="secondary" onclick={resetForm}>Отмена</Button>
|
<Button size="sm" type="button" color="secondary" onclick={resetForm}>Отмена</Button>
|
||||||
|
{/if}
|
||||||
<span class="flex-grow-1"></span>
|
<span class="flex-grow-1"></span>
|
||||||
<Button
|
{#if isEditing}
|
||||||
type="button"
|
<Button color="danger" size="sm" type="button" onclick={() => confirmDeletePoint(newPoint)}>
|
||||||
color="danger"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => isConfirmationVisible = true}
|
|
||||||
>
|
|
||||||
Удалить точку
|
Удалить точку
|
||||||
</Button>
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
resetForm();
|
||||||
|
closeModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Закрыть без сохранения
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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,
|
Icon,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
import { PROFILE_MAP } from "$lib/types";
|
import { PREDICTION_MODE_MAP, PPREDICTION_MODE_NAMES } from "$lib/types";
|
||||||
import { FlightParametersStore, writeLocalStorage, ScenarioStore, SavedScenarioTemplatesStore } from "$lib/stores";
|
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 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 = () => {
|
export const collapsePanel = () => {
|
||||||
isCollapsed = true;
|
isCollapsed = true;
|
||||||
|
|
@ -41,10 +161,10 @@
|
||||||
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
||||||
style="width:100%;"
|
style="width:100%;"
|
||||||
aria-label="Свернуть/развернуть параметры прогнозирования"
|
aria-label="Свернуть/развернуть параметры прогнозирования"
|
||||||
on:click={() => (isCollapsed = !isCollapsed)}
|
onclick={() => (isCollapsed = !isCollapsed)}
|
||||||
>
|
>
|
||||||
<b class="card-title mb-0 text-white p-0">Сценарий прогнозирования</b>
|
<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}
|
{#if isCollapsed}
|
||||||
<Icon name="caret-left-fill" class="text-white" />
|
<Icon name="caret-left-fill" class="text-white" />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -58,50 +178,77 @@
|
||||||
<FormGroup spacing="mb-2">
|
<FormGroup spacing="mb-2">
|
||||||
<Label for="scenarioName" class="form-label">Cценарий:</Label>
|
<Label for="scenarioName" class="form-label">Cценарий:</Label>
|
||||||
<InputGroup size="sm">
|
<InputGroup size="sm">
|
||||||
|
<div class="position-relative flex-grow-1">
|
||||||
<SelectSearchable
|
<SelectSearchable
|
||||||
id="startPoint"
|
style="min-height: calc(1.5em + .5rem + 2px); padding: .25rem .5rem; font-size: .875rem;"
|
||||||
options={$SavedScenarioTemplatesStore.map(scenario => ({
|
id="cp-start-point"
|
||||||
|
options={$SavedScenarioStore.map((scenario) => ({
|
||||||
value: scenario.id,
|
value: scenario.id,
|
||||||
label: scenario.name,
|
label:
|
||||||
|
scenario.name +
|
||||||
|
`${scenario.id == $ScenarioStore.id && scenarioUnsaved ? " (изменено)" : ""}`,
|
||||||
}))}
|
}))}
|
||||||
placeholder="Выберите сценарий..."
|
bind:selected={selectedScenarioId}
|
||||||
|
placeholder="Новый сценарий..."
|
||||||
searchPlaceholder="Поиск сценариев..."
|
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>
|
<span>✓</span>
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<div class="d-flex gap-2 mb-2">
|
<div class="d-flex gap-2 mb-2">
|
||||||
<Button class="flex-fill" color="secondary" size="sm">
|
<Button color="secondary flex-fill" size="sm" title="Открыть список сценариев">
|
||||||
Сохранить
|
Все сценарии
|
||||||
<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"
|
|
||||||
>
|
|
||||||
Редактировать сохраненные сценарии
|
|
||||||
<Icon name="journal-bookmark-fill" />
|
<Icon name="journal-bookmark-fill" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="primary flex-fill"
|
||||||
|
size="sm"
|
||||||
|
title="Сохранить текущие условия как сценарий"
|
||||||
|
onclick={handleSaveCurrentScenario}
|
||||||
|
disabled={!scenarioUnsaved && selectedScenarioId !== -1}
|
||||||
|
>
|
||||||
|
{selectedScenarioId !== -1 ? "Обновить сценарий" : "Сохранить сценарий"}
|
||||||
|
<Icon name="floppy2-fill" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<FormGroup spacing="mb-2">
|
<FormGroup spacing="mb-2">
|
||||||
<Label for="scenarioMode" class="form-label">Режим сценария:</Label>
|
<Label for="scenarioMode" class="form-label">Режим сценария:</Label>
|
||||||
<InputGroup size="sm">
|
<InputGroup size="sm">
|
||||||
<Input type="select" id="scenarioMode">
|
<Input type="select" id="scenarioMode" bind:value={$ScenarioStore.template_data.prediction_mode}
|
||||||
<option>Обычный</option>
|
on:change={() => {
|
||||||
<option>Почасовой</option>
|
scenarioUnsaved = true;
|
||||||
<option>Ансамблевый</option>
|
}}>
|
||||||
|
{#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>
|
</Input>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
@ -141,7 +288,7 @@
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
title="Edit Saved Locations"
|
title="Edit Saved Locations"
|
||||||
on:click={() => console.log("Not implemented yet")}
|
onclick={() => console.log("Not implemented yet")}
|
||||||
>
|
>
|
||||||
<span>Экспорт</span>
|
<span>Экспорт</span>
|
||||||
<Icon name="file-earmark-arrow-down" />
|
<Icon name="file-earmark-arrow-down" />
|
||||||
|
|
@ -151,3 +298,4 @@
|
||||||
</CardBody>
|
</CardBody>
|
||||||
{/if}
|
{/if}
|
||||||
</Card>
|
</Card>
|
||||||
|
<ScenarioEditor bind:this={scenarioEditorRef} onSave={handleModalSave} />
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
onChange?: (value: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -74,6 +75,9 @@
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
dispatch('change', selected);
|
dispatch('change', selected);
|
||||||
|
if (restProps.onChange) {
|
||||||
|
restProps.onChange(selected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
|
@ -129,7 +133,7 @@
|
||||||
{#each filteredOptions as option}
|
{#each filteredOptions as option}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="dropdown-item "
|
class="dropdown-item small"
|
||||||
class:active={option.value === selected}
|
class:active={option.value === selected}
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,11 @@ function formatLaunchDateTime(dateObj: string | Date, timeStr: string): string {
|
||||||
|
|
||||||
export const getForecast = async (
|
export const getForecast = async (
|
||||||
flightParameters: Record<string, any>,
|
flightParameters: Record<string, any>,
|
||||||
|
launchDateTime: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
// Create request object
|
// Create request object
|
||||||
flightParameters.dataset = getLatestDataset();
|
flightParameters.dataset = getLatestDataset();
|
||||||
|
flightParameters.launch_datetime = launchDateTime;
|
||||||
|
|
||||||
console.log("Sending request:", flightParameters);
|
console.log("Sending request:", flightParameters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +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, SavedFlightProfile, SavedScenarioTemplate, TemplateData } from "./types";
|
import type { SavedPoint, SavedFlightProfile, SavedScenario, TemplateData } 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);
|
||||||
|
|
@ -42,7 +42,6 @@ const flightParametersDefaults: FlightParameters = {
|
||||||
descent_rate: 5.0,
|
descent_rate: 5.0,
|
||||||
format: "json",
|
format: "json",
|
||||||
launch_altitude: 0.0,
|
launch_altitude: 0.0,
|
||||||
launch_datetime: "",
|
|
||||||
launch_latitude: 62.1234,
|
launch_latitude: 62.1234,
|
||||||
launch_longitude: 129.1234,
|
launch_longitude: 129.1234,
|
||||||
profile: "standard_profile",
|
profile: "standard_profile",
|
||||||
|
|
@ -55,14 +54,14 @@ export const FlightParametersStore = writable<FlightParameters>(
|
||||||
|
|
||||||
const templateDataDefaults: TemplateData = {
|
const templateDataDefaults: TemplateData = {
|
||||||
description: "",
|
description: "",
|
||||||
prediction_mode: false,
|
prediction_mode: "",
|
||||||
model: "",
|
model: "",
|
||||||
dataset: "",
|
dataset: "",
|
||||||
flight_parameters: flightParametersDefaults,
|
flight_parameters: flightParametersDefaults,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScenarioStore = writable<TemplateData>(
|
export const ScenarioStore = writable<SavedScenario>(
|
||||||
readLocalStorage<TemplateData>("scenario", templateDataDefaults)
|
readLocalStorage<SavedScenario>("scenario", {} as SavedScenario)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RawTelemetryStore = writable<RawTelemetry>(
|
export const RawTelemetryStore = writable<RawTelemetry>(
|
||||||
|
|
@ -87,4 +86,4 @@ export const SavedPointsStore = writable<SavedPoint[]>([]);
|
||||||
export const SavedFlightProfilesStore = writable<SavedFlightProfile[]>([]);
|
export const SavedFlightProfilesStore = writable<SavedFlightProfile[]>([]);
|
||||||
|
|
||||||
// stub
|
// stub
|
||||||
export const SavedScenarioTemplatesStore = writable<SavedScenarioTemplate[]>([]);
|
export const SavedScenarioStore = writable<SavedScenario[]>([]);
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import type { LatLngExpression, LatLngLiteral } from "leaflet";
|
import type { LatLngExpression, LatLngLiteral } from "leaflet";
|
||||||
|
|
||||||
export const PROFILE_MAP = {
|
export const PROFILE_MAP = {
|
||||||
Normal: "standard_profile",
|
"Обычный": "standard_profile",
|
||||||
Float: "float_profile",
|
"Дрейф": "float_profile",
|
||||||
Reverse: "reverse_profile",
|
"Реверсивный": "reverse_profile",
|
||||||
Custom: "custom_profile",
|
"Пользовательский": "custom_profile",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map of profile names to their string identifiers
|
// Map of profile names to their string identifiers
|
||||||
export const PROFILE_NAMES = {
|
export const PROFILE_NAMES = {
|
||||||
standard_profile: "Normal",
|
standard_profile: "Обычный",
|
||||||
float_profile: "Float",
|
float_profile: "Дрейф",
|
||||||
reverse_profile: "Reverse",
|
reverse_profile: "Реверсивный",
|
||||||
custom_profile: "Custom",
|
custom_profile: "Пользовательский"
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProfileName = keyof typeof PROFILE_MAP;
|
export type ProfileName = keyof typeof PROFILE_MAP;
|
||||||
|
|
@ -26,7 +26,6 @@ export interface FlightParameters {
|
||||||
descent_rate: number;
|
descent_rate: number;
|
||||||
format: "json";
|
format: "json";
|
||||||
launch_altitude: number;
|
launch_altitude: number;
|
||||||
launch_datetime: string;
|
|
||||||
launch_latitude: number;
|
launch_latitude: number;
|
||||||
launch_longitude: number;
|
launch_longitude: number;
|
||||||
profile: (typeof PROFILE_MAP)[ProfileName];
|
profile: (typeof PROFILE_MAP)[ProfileName];
|
||||||
|
|
@ -36,9 +35,22 @@ export interface FlightParameters {
|
||||||
template?: number; // Optional, used for saved scenarios
|
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 {
|
export interface TemplateData {
|
||||||
description: string;
|
description: string;
|
||||||
prediction_mode: boolean;
|
prediction_mode: string;
|
||||||
model: string;
|
model: string;
|
||||||
dataset: string;
|
dataset: string;
|
||||||
flight_parameters: FlightParameters;
|
flight_parameters: FlightParameters;
|
||||||
|
|
@ -120,7 +132,7 @@ export interface SavedFlightProfile {
|
||||||
rate_profile_data: object;
|
rate_profile_data: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SavedScenarioTemplate {
|
export interface SavedScenario {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
template_data: TemplateData;
|
template_data: TemplateData;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import PanelContainer from "$lib/components/PanelContainer.svelte";
|
import PanelContainer from "$lib/components/PanelContainer.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 PointEditor from "$lib/components/PointEditor.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { PredictionStore } from "$lib/stores";
|
import { PredictionStore } from "$lib/stores";
|
||||||
import { addToast, removeToast } from "$lib/components/Toast.svelte";
|
import { addToast, removeToast } from "$lib/components/Toast.svelte";
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if activeTab === 'control'}
|
{#if activeTab === 'control'}
|
||||||
<ControlPanel {handleClickSelectOnMap} bind:this={controlPanel} />
|
<ControlPanel onSelectOnMapClick={handleClickSelectOnMap} bind:this={controlPanel} />
|
||||||
{:else if activeTab === 'scenario'}
|
{:else if activeTab === 'scenario'}
|
||||||
<ScenarioPanel />
|
<ScenarioPanel />
|
||||||
{:else if activeTab === 'settings'}
|
{:else if activeTab === 'settings'}
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@
|
||||||
import { addToast } from "$lib/components/Toast.svelte";
|
import { addToast } from "$lib/components/Toast.svelte";
|
||||||
|
|
||||||
// TODO: Implement these imports
|
// 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 { getSavedPoints, deletePoint } from "$lib/api/points";
|
||||||
import { getSavedFlightProfiles, deleteFlightProfile } from "$lib/api/profiles";
|
import { getSavedFlightProfiles, deleteFlightProfile } from "$lib/api/profiles";
|
||||||
import { getSavedScenarioTemplates, deleteScenarioTemplate } from "$lib/api/templates";
|
import { getSavedScenarios, deleteScenario } from "$lib/api/scenarios";
|
||||||
import type { SavedPoint, SavedFlightProfile, SavedScenarioTemplate } from "$lib/types";
|
import type { SavedPoint, SavedFlightProfile, SavedScenario } from "$lib/types";
|
||||||
|
|
||||||
// Table handlers
|
// Table handlers
|
||||||
let pointsTable = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 5 }));
|
let pointsTable = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 5 }));
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
let profilesTable = $derived(new TableHandler($SavedFlightProfilesStore, { rowsPerPage: 5 }));
|
let profilesTable = $derived(new TableHandler($SavedFlightProfilesStore, { rowsPerPage: 5 }));
|
||||||
let profilesSearch = $derived(profilesTable.createSearch(["name"]));
|
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 templatesSearch = $derived(templatesTable.createSearch(["name"]));
|
||||||
|
|
||||||
let editPoint: SavedPoint | null = $state(null);
|
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: 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} },
|
{ 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
|
// TODO: Uncomment when API is ready
|
||||||
|
|
@ -374,6 +371,9 @@
|
||||||
point={editPoint}
|
point={editPoint}
|
||||||
isOpen={editPoint !== null}
|
isOpen={editPoint !== null}
|
||||||
onClose={() => { editPoint = null; pointsTable.setRows($SavedPointsStore) }}
|
onClose={() => { editPoint = null; pointsTable.setRows($SavedPointsStore) }}
|
||||||
|
editor={true}
|
||||||
|
closeOnSave={true}
|
||||||
|
closeOnDelete={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
Loading…
Add table
Add a link
Reference in a new issue