302 lines
9.5 KiB
Svelte
302 lines
9.5 KiB
Svelte
<script lang="ts">
|
||
import {
|
||
Card,
|
||
CardHeader,
|
||
CardBody,
|
||
Button,
|
||
FormGroup,
|
||
Label,
|
||
Input,
|
||
InputGroup,
|
||
InputGroupText,
|
||
Icon,
|
||
} from "@sveltestrap/sveltestrap";
|
||
|
||
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/ui/SelectSearchable.svelte";
|
||
import { onMount } from "svelte";
|
||
import { addToast } from "./ui/Toast.svelte";
|
||
import ScenarioEditor from "$lib/components/editors/ScenarioEditor.svelte";
|
||
|
||
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 savedFlightParameters = savedScenario.flight_parameters;
|
||
|
||
// Compare flight parameters excluding launch_datetime
|
||
console.log("Comparing flight parameters:", flightParameters, savedFlightParameters);
|
||
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: "",
|
||
flight_parameters: $FlightParametersStore,
|
||
description: "test",
|
||
model: "test",
|
||
dataset: "test",
|
||
prediction_mode: $ScenarioStore.prediction_mode,
|
||
},
|
||
true,
|
||
handleModalSave,
|
||
);
|
||
}
|
||
}
|
||
|
||
function handleApplySelectedScenario(showToast = true) {
|
||
const selectedScenario = $SavedScenarioStore.find((scenario) => scenario.id === selectedScenarioId);
|
||
if (selectedScenario) {
|
||
$ScenarioStore = selectedScenario;
|
||
$FlightParametersStore = selectedScenario.flight_parameters;
|
||
scenarioUnsaved = false;
|
||
writeLocalStorage("scenario", $ScenarioStore);
|
||
writeLocalStorage("flightParameters", $FlightParametersStore);
|
||
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;
|
||
};
|
||
|
||
export const expandPanel = () => {
|
||
isCollapsed = false;
|
||
};
|
||
|
||
export const togglePanel = () => {
|
||
isCollapsed = !isCollapsed;
|
||
};
|
||
</script>
|
||
|
||
<Card>
|
||
<CardHeader
|
||
class="bg-primary text-white d-flex justify-content-between align-items-center card-header p-1 px-3"
|
||
style="cursor:pointer;">
|
||
<button
|
||
type="button"
|
||
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
||
style="width:100%;"
|
||
aria-label="Свернуть/развернуть параметры прогнозирования"
|
||
onclick={() => (isCollapsed = !isCollapsed)}>
|
||
<b class="card-title mb-0 text-white p-0">Сценарий прогнозирования</b>
|
||
<Button class="p-0" size="sm" color="primary" onclick={() => (isCollapsed = !isCollapsed)}>
|
||
{#if isCollapsed}
|
||
<Icon name="caret-left-fill" class="text-white" />
|
||
{:else}
|
||
<Icon name="caret-down-fill" class="text-white" />
|
||
{/if}
|
||
</Button>
|
||
</button>
|
||
</CardHeader>
|
||
{#if !isCollapsed}
|
||
<CardBody>
|
||
<FormGroup spacing="mb-2">
|
||
<Label for="scenarioName" class="form-label">Cценарий:</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"
|
||
options={$SavedScenarioStore.map((scenario) => ({
|
||
value: scenario.id,
|
||
label:
|
||
scenario.name +
|
||
`${scenario.id == $ScenarioStore.id && scenarioUnsaved ? " (изменено)" : ""}`,
|
||
}))}
|
||
bind:selected={selectedScenarioId}
|
||
placeholder="Новый сценарий..."
|
||
searchPlaceholder="Поиск сценариев..."
|
||
clearable={true}
|
||
onChange={() => {
|
||
if (!scenarioUnsaved) {
|
||
handleApplySelectedScenario(false);
|
||
}
|
||
}} />
|
||
<!-- <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 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"
|
||
bind:value={$ScenarioStore.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>
|
||
|
||
<FormGroup spacing="mb-2">
|
||
<Label for="scenarioMode" class="form-label">Модель атмосферы:</Label>
|
||
<InputGroup size="sm">
|
||
<Input type="select" id="scenarioMode">
|
||
<option>GFS (0.25°)</option>
|
||
<option>GFS (0.5°)</option>
|
||
</Input>
|
||
</InputGroup>
|
||
</FormGroup>
|
||
|
||
<FormGroup spacing="mb-2">
|
||
<Label for="scenarioMode" class="form-label">Набор данных:</Label>
|
||
<InputGroup size="sm">
|
||
<Input type="select" id="scenarioMode">
|
||
<option>Выбрать автоматически</option>
|
||
<!-- TODO ручка апи для доступных наборов -->
|
||
<option>20250701-00</option>
|
||
<option>20250701-06</option>
|
||
</Input>
|
||
</InputGroup>
|
||
</FormGroup>
|
||
|
||
<hr />
|
||
|
||
<FormGroup spacing="mb-0">
|
||
<Label for="export" class="form-label">Экспортировать результат:</Label>
|
||
<InputGroup size="sm">
|
||
<Input type="select" id="export">
|
||
<option>JSON</option>
|
||
<option>CSV</option>
|
||
<option>KML</option>
|
||
</Input>
|
||
<Button
|
||
color="primary"
|
||
title="Edit Saved Locations"
|
||
onclick={() => console.log("Not implemented yet")}>
|
||
<span>Экспорт</span>
|
||
<Icon name="file-earmark-arrow-down" />
|
||
</Button>
|
||
</InputGroup>
|
||
</FormGroup>
|
||
</CardBody>
|
||
{/if}
|
||
</Card>
|
||
<ScenarioEditor bind:this={scenarioEditorRef} onSave={handleModalSave} />
|