control panel ux
This commit is contained in:
parent
a1d80eb984
commit
7d01fce094
8 changed files with 219 additions and 60 deletions
|
|
@ -12,14 +12,15 @@
|
|||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import PointListModal from "$lib/components/PointListModal.svelte";
|
||||
import SelectSearchable from "$lib/components/SelectSearchable.svelte";
|
||||
import { getForecast } from "$lib/prediction";
|
||||
import type { FlightParameters, ProfileName, ProfileIdentifier } from "$lib/types";
|
||||
import type { FlightParameters, ProfileName, ProfileIdentifier, SavedPoint } from "$lib/types";
|
||||
import { PROFILE_MAP, PROFILE_NAMES } from "$lib/types";
|
||||
import { SavedPointsStore, FlightParametersStore, writeLocalStorage } from "$lib/stores";
|
||||
import { getSavedPoints } from "$lib/api/points";
|
||||
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() {
|
||||
|
|
@ -54,21 +55,22 @@
|
|||
|
||||
interface Props {
|
||||
handleClickSelectOnMap?: () => void;
|
||||
handleClickPointListModal?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
handleClickSelectOnMap = () => console.log("Select on map clicked"),
|
||||
handleClickPointListModal = () => console.log("Open Point List Modal"),
|
||||
}: Props = $props();
|
||||
|
||||
// State
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
let pointListModal: PointListModal | null = null;
|
||||
|
||||
// Initialize date/time
|
||||
const now = new Date();
|
||||
let startDate = $state(now.toISOString().split("T")[0]);
|
||||
let startTime = $state(now.toISOString().split("T")[1].split(".")[0]);
|
||||
let selectedPoint = $state($FlightParametersStore.start_point); // Default to custom point
|
||||
|
||||
// Coordinate inputs
|
||||
let inputLat = $derived($FlightParametersStore.start_point === -1
|
||||
|
|
@ -87,21 +89,73 @@
|
|||
}
|
||||
}
|
||||
|
||||
function applyCoordinatesFromInput() {
|
||||
const lat = parseFloat(inputLat);
|
||||
const lng = parseFloat(inputLng);
|
||||
const alt = parseFloat(inputAlt);
|
||||
function applySeletedPoint() {
|
||||
if (selectedPoint && selectedPoint !== -1) {
|
||||
$FlightParametersStore.start_point = selectedPoint;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
function saveCurrentPoint() {
|
||||
if (selectedPoint !== -1) {
|
||||
const point = $SavedPointsStore.find(p => p.id === selectedPoint);
|
||||
if (point) {
|
||||
updatePoint(point)
|
||||
.then((updatedPoint) => {
|
||||
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
addToast({
|
||||
header: "Точка обновлена",
|
||||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||
color: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Ошибка обновления точки",
|
||||
body: `Ошибка при обновлении точки: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
console.error("Ошибка при обновлении точки:", error);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid coordinate input");
|
||||
// TODO: Show validation error to user
|
||||
pointListModal?.openModalAndCreate(null, {
|
||||
id: 0,
|
||||
name: `Новая точка ${new Date().toLocaleString()}`,
|
||||
lat: parseFloat(inputLat),
|
||||
lon: parseFloat(inputLng),
|
||||
alt: parseFloat(inputAlt),
|
||||
}, true, onModalSave);
|
||||
}
|
||||
}
|
||||
|
||||
function onModalSave(savedPoint: SavedPoint) {
|
||||
if (savedPoint) {
|
||||
$FlightParametersStore.start_point = savedPoint.id;
|
||||
selectedPoint = savedPoint.id;
|
||||
setToCustomOnChange();
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickPointListModal() {
|
||||
pointListModal?.openModal();
|
||||
}
|
||||
|
||||
// function applyCoordinatesFromInput() {
|
||||
// const lat = parseFloat(inputLat);
|
||||
// const lng = parseFloat(inputLng);
|
||||
// const alt = parseFloat(inputAlt);
|
||||
|
||||
// if (!isNaN(lat) && !isNaN(lng)) {
|
||||
// $FlightParametersStore.launch_latitude = lat;
|
||||
// $FlightParametersStore.launch_longitude = lng;
|
||||
// $FlightParametersStore.launch_altitude = alt || 0; // Default to 0 if alt is NaN
|
||||
// } else {
|
||||
// console.error("Invalid coordinate input");
|
||||
// // TODO: Show validation error to user
|
||||
// }
|
||||
// }
|
||||
|
||||
async function handleGetPrediction() {
|
||||
$FlightParametersStore.launch_datetime = `${startDate}T${startTime}Z`;
|
||||
$FlightParametersStore.launch_latitude = parseFloat(inputLat);
|
||||
|
|
@ -173,6 +227,8 @@
|
|||
});
|
||||
return [];
|
||||
});
|
||||
selectedPoint = $FlightParametersStore.start_point;
|
||||
console.log("ControlPanel mounted", selectedPoint);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -239,9 +295,11 @@
|
|||
<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={$FlightParametersStore.start_point}
|
||||
bind:selected={selectedPoint}
|
||||
options={$SavedPointsStore.map(point => ({
|
||||
value: point.id,
|
||||
label: point.name,
|
||||
|
|
@ -249,13 +307,49 @@
|
|||
placeholder="Выберите точку старта"
|
||||
searchPlaceholder="Поиск точки..."
|
||||
/>
|
||||
<Button color="secondary" title="Edit Saved Locations" onclick={handleClickPointListModal}>
|
||||
<span>Редакт.</span>
|
||||
<Icon name="journal-bookmark-fill" />
|
||||
<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">
|
||||
|
|
@ -274,18 +368,11 @@
|
|||
placeholder="Longitude"
|
||||
onchange={setToCustomOnChange}
|
||||
/>
|
||||
<Button color="success" size="sm" onclick={applyCoordinatesFromInput} title="Apply Coordinates">
|
||||
✓
|
||||
<Button color="secondary" size="sm" onclick={handleClickSelectOnMap}>
|
||||
<Icon name="geo-alt-fill" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Button color="outline-secondary" size="sm" class="w-100" onclick={handleClickSelectOnMap}>
|
||||
Указать на карте
|
||||
</Button>
|
||||
</FormGroup>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="startHeight" class="form-label">Высота старта (м):</Label>
|
||||
|
|
@ -338,3 +425,4 @@
|
|||
</CardBody>
|
||||
{/if}
|
||||
</Card>
|
||||
<PointListModal bind:this={pointListModal} />
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
isOpen = $bindable(false),
|
||||
onClose = () => {},
|
||||
onChange = () => {},
|
||||
onSave = () => {},
|
||||
point = null,
|
||||
coordinates = { id: 0, name: "", lat: 0, lon: 0, alt: 0 },
|
||||
} = $props();
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
// 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);
|
||||
|
|
@ -56,8 +58,29 @@
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +113,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
function handleSavePoint() {
|
||||
export function handleSavePoint() {
|
||||
if (isEditing && selectedPoint) {
|
||||
updatePoint(newPoint)
|
||||
.then((updatedPoint) => {
|
||||
|
|
@ -102,6 +125,10 @@
|
|||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||
color: "success",
|
||||
});
|
||||
if (closeOnSave) {
|
||||
closeModal();
|
||||
}
|
||||
onSave(updatedPoint);
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при обновлении точки: ${error.message}`);
|
||||
|
|
@ -117,6 +144,10 @@
|
|||
body: `Точка "${savedPoint.name}" успешно сохранена.`,
|
||||
color: "success",
|
||||
});
|
||||
if (closeOnSave) {
|
||||
closeModal();
|
||||
}
|
||||
onSave(savedPoint);
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при сохранении точки: ${error.message}`);
|
||||
|
|
@ -143,7 +174,15 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal {isOpen} toggle={closeModal} size="lg" fade={false} backdrop={true} scrollable class={isConfirmationVisible ? "modal-tinted" : ""}>
|
||||
<Modal
|
||||
{isOpen}
|
||||
toggle={closeModal}
|
||||
size="lg"
|
||||
fade={false}
|
||||
backdrop={true}
|
||||
scrollable
|
||||
class={isConfirmationVisible ? "modal-tinted" : ""}
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Сохраненные точки</h5>
|
||||
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@
|
|||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { PROFILE_MAP } from "$lib/types";
|
||||
import { FlightParametersStore, writeLocalStorage } from "$lib/stores";
|
||||
import { FlightParametersStore, writeLocalStorage, ScenarioStore, SavedScenarioTemplatesStore } from "$lib/stores";
|
||||
import SelectSearchable from "$lib/components/SelectSearchable.svelte";
|
||||
|
||||
let isCollapsed = false;
|
||||
|
||||
|
|
@ -55,9 +56,20 @@
|
|||
{#if !isCollapsed}
|
||||
<CardBody>
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioName" class="form-label">Название сценария:</Label>
|
||||
<Label for="scenarioName" class="form-label">Cценарий:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input id="scenarioName" type="text" />
|
||||
<SelectSearchable
|
||||
id="startPoint"
|
||||
options={$SavedScenarioTemplatesStore.map(scenario => ({
|
||||
value: scenario.id,
|
||||
label: scenario.name,
|
||||
}))}
|
||||
placeholder="Выберите сценарий..."
|
||||
searchPlaceholder="Поиск сценариев..."
|
||||
/>
|
||||
<Button color="success" title="Применить сценарий">
|
||||
<span>✓</span>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
|
|
|
|||
0
src/lib/components/ScenarioTemplateEditor.svelte
Normal file
0
src/lib/components/ScenarioTemplateEditor.svelte
Normal file
|
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { onMount } from 'svelte';
|
||||
import { on } from 'svelte/events';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
options?: { value: any; label:string }[];
|
||||
|
|
@ -40,6 +42,11 @@
|
|||
options.find(opt => opt.value === selected)?.label || ''
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
// Update dropdown position on mount
|
||||
updateDropdownPosition();
|
||||
});
|
||||
|
||||
function updateDropdownPosition() {
|
||||
if (!selectElement) return;
|
||||
const rect = selectElement.getBoundingClientRect();
|
||||
|
|
@ -106,7 +113,7 @@
|
|||
<span class:text-muted={!selected}>{selectedLabel || placeholder}</span>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="dropdown-menu show" bind:this={dropdownElement} style={dropdownStyle}>
|
||||
<div class="pt-0 dropdown-menu show" bind:this={dropdownElement} style={dropdownStyle}>
|
||||
<div class="p-2">
|
||||
<input
|
||||
type="text"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { writable } from "svelte/store";
|
||||
import type { FlightParameters, RawTelemetry, Telemetry } from "./types";
|
||||
import type { RawPrediction, Prediction } from "./types";
|
||||
import type { SavedPoint, SavedFlightProfile, SavedScenarioTemplate } from "./types";
|
||||
import type { SavedPoint, SavedFlightProfile, SavedScenarioTemplate, TemplateData } from "./types";
|
||||
|
||||
export const readLocalStorage = <T>(key: string, defaultValue: T): T => {
|
||||
const item = localStorage.getItem(key);
|
||||
|
|
@ -53,6 +53,18 @@ export const FlightParametersStore = writable<FlightParameters>(
|
|||
readLocalStorage<FlightParameters>("flightParameters", flightParametersDefaults)
|
||||
);
|
||||
|
||||
const templateDataDefaults: TemplateData = {
|
||||
description: "",
|
||||
prediction_mode: false,
|
||||
model: "",
|
||||
dataset: "",
|
||||
flight_parameters: flightParametersDefaults,
|
||||
};
|
||||
|
||||
export const ScenarioStore = writable<TemplateData>(
|
||||
readLocalStorage<TemplateData>("scenario", templateDataDefaults)
|
||||
);
|
||||
|
||||
export const RawTelemetryStore = writable<RawTelemetry>(
|
||||
readLocalStorage<RawTelemetry>("rawTelemetry", {} as RawTelemetry)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,8 +32,19 @@ export interface FlightParameters {
|
|||
profile: (typeof PROFILE_MAP)[ProfileName];
|
||||
version: number;
|
||||
start_point?: number; // Optional, used for saved points
|
||||
rate_profile?: number; // Optional, used for custom profiles
|
||||
template?: number; // Optional, used for saved scenarios
|
||||
}
|
||||
|
||||
export interface TemplateData {
|
||||
description: string;
|
||||
prediction_mode: boolean;
|
||||
model: string;
|
||||
dataset: string;
|
||||
flight_parameters: FlightParameters;
|
||||
}
|
||||
|
||||
|
||||
export interface Point {
|
||||
latlng: LatLngLiteral & { alt?: number };
|
||||
datetime: Date;
|
||||
|
|
@ -112,5 +123,5 @@ export interface SavedFlightProfile {
|
|||
export interface SavedScenarioTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
template_data: object;
|
||||
template_data: TemplateData;
|
||||
}
|
||||
|
|
@ -18,8 +18,6 @@
|
|||
let selectionToastId: string | null = null;
|
||||
let activeTab: 'control' | 'scenario' | 'settings' | 'about' = 'scenario';
|
||||
|
||||
let pointListModal: PointListModal | null = null;
|
||||
|
||||
onMount(() => {
|
||||
PredictionStore.subscribe((data) => {
|
||||
if (data) {
|
||||
|
|
@ -43,8 +41,8 @@
|
|||
console.log("Selection mode enabled");
|
||||
if (!selectionToastId) {
|
||||
selectionToastId = addToast({
|
||||
header: "Selection Mode",
|
||||
body: "Click on the map to select a position.",
|
||||
header: "Режим выбора координат",
|
||||
body: "Кликните на карту, чтобы выбрать координаты",
|
||||
color: "info",
|
||||
persistent: true,
|
||||
onRemoveCallback: () => {
|
||||
|
|
@ -67,13 +65,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleClickPointListModal() {
|
||||
if (map) {
|
||||
map.stopSelection();
|
||||
console.log("Selection mode disabled");
|
||||
}
|
||||
pointListModal?.openModal();
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
|
@ -95,7 +86,7 @@
|
|||
|
||||
<div>
|
||||
{#if activeTab === 'control'}
|
||||
<ControlPanel {handleClickSelectOnMap} {handleClickPointListModal} bind:this={controlPanel} />
|
||||
<ControlPanel {handleClickSelectOnMap} bind:this={controlPanel} />
|
||||
{:else if activeTab === 'scenario'}
|
||||
<ScenarioPanel />
|
||||
{:else if activeTab === 'settings'}
|
||||
|
|
@ -106,6 +97,5 @@
|
|||
</div>
|
||||
</PanelContainer>
|
||||
<ToastContainer />
|
||||
<PointListModal bind:this={pointListModal} />
|
||||
</Map>
|
||||
</main>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue