Minimal error handling

This commit is contained in:
ThePetrovich 2025-07-02 19:00:26 +08:00
parent 1a89d49e8a
commit 1e09b2d7ef
4 changed files with 184 additions and 121 deletions

View file

@ -13,53 +13,105 @@
} from "@sveltestrap/sveltestrap";
import { getForecast } from "$lib/prediction";
import type { FlightParameters, ProfileName } from "$lib/types";
import { PROFILE_MAP } from "$lib/types";
import type { FlightParameters, ProfileName, ProfileIdentifier } 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 type { SavedPoint } from "$lib/types";
import { onMount } from "svelte";
import { addToast } from "$lib/components/Toast.svelte";
let isCollapsed = false;
let selectedProfile: ProfileName = "Normal";
let startPoint = "Custom";
// 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]
// };
// }
let element: HTMLDivElement | null = null;
// 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;
// }
// }
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 startPoint = $state("Custom");
// Initialize date/time
const now = new Date();
let startDate = now.toISOString().split("T")[0]; // YYYY-MM-DD
let startTime = now.toISOString().split("T")[1].split(".")[0]; // HH:MM:SS
let startDate = $state(now.toISOString().split("T")[0]);
let startTime = $state(now.toISOString().split("T")[1].split(".")[0]);
let inputLat = $FlightParametersStore.launch_latitude.toFixed(6).toString();
let inputLng = $FlightParametersStore.launch_longitude.toFixed(6).toString();
// Coordinate inputs
let inputLat = $state($FlightParametersStore.launch_latitude.toFixed(6));
let inputLng = $state($FlightParametersStore.launch_longitude.toFixed(6));
$: $FlightParametersStore = {
...$FlightParametersStore,
profile: PROFILE_MAP[selectedProfile],
};
// Reactive effects
$effect(() => {
$FlightParametersStore;
});
$: $SavedPointsStore, setCoordinatesFromSavedPoint();
$effect(() => {
// Watch for saved points changes
$SavedPointsStore;
// This effect also depends on startPoint, which is changed by setCoordinatesFromSavedPoint
// via other state variables. To avoid an infinite loop, we only call this when startPoint changes.
const sp = startPoint;
setCoordinatesFromSavedPoint();
});
// Functions
function setCoordinatesFromSavedPoint() {
console.log("Start point changed:", startPoint);
if (startPoint === "Custom") {
$FlightParametersStore.launch_latitude = parseFloat(inputLat);
$FlightParametersStore.launch_longitude = parseFloat(inputLng);
} else {
const lat = parseFloat(inputLat);
const lng = parseFloat(inputLng);
if (!isNaN(lat) && !isNaN(lng)) {
$FlightParametersStore.launch_latitude = lat;
$FlightParametersStore.launch_longitude = lng;
}
else {
const selectedOption = document.querySelector(
`#startPoint option[value="${startPoint}"]`
) as HTMLOptionElement;
if (selectedOption) {
const lat = parseFloat(selectedOption.getAttribute("data-lat") || "0");
const lng = parseFloat(selectedOption.getAttribute("data-lng") || "0");
const alt = parseFloat(selectedOption.getAttribute("data-alt") || "0");
inputLat = lat.toFixed(6).toString();
inputLng = lng.toFixed(6).toString();
inputLat = lat.toFixed(6);
inputLng = lng.toFixed(6);
$FlightParametersStore.launch_latitude = lat;
$FlightParametersStore.launch_longitude = lng;
$FlightParametersStore.launch_altitude = alt;
console.log("Updated position from saved point:", lat, lng, alt);
}
}
}
@ -67,102 +119,84 @@
function setToCustomOnChange() {
if (startPoint !== "Custom") {
startPoint = "Custom";
console.log("Switched to Custom point");
}
}
onMount(async () => {
// Load saved points from the server or local storage
const savedPoints = await getSavedPoints();
SavedPointsStore.set(savedPoints);
});
const handleGetPrediction = async () => {
console.log("Fetching prediction with parameters:", $FlightParametersStore);
console.log(startDate, startTime);
$FlightParametersStore.launch_datetime = `${startDate}T${startTime}Z`;
writeLocalStorage<FlightParameters>("flightParameters", $FlightParametersStore);
try {
const response = await getForecast($FlightParametersStore);
console.log(response);
// TODO: Notify other components of the new prediction.
// const dispatch = createEventDispatcher();
// dispatch('newPrediction', response);
} catch (error) {
console.error("Error fetching forecast:", error);
// TODO: Display a user-friendly error message in the UI.
}
};
export let handleClickSelectOnMap = () => {
console.log("Select on map clicked");
}
export let handleClickPointListModal = () => {
console.log("Open Point List Modal");
};
const applyCoordinatesFromInput = () => {
function applyCoordinatesFromInput() {
const lat = parseFloat(inputLat);
const lng = parseFloat(inputLng);
if (!isNaN(lat) && !isNaN(lng)) {
$FlightParametersStore.launch_latitude = lat;
$FlightParametersStore.launch_longitude = lng;
console.log(
"Updated position:",
$FlightParametersStore.launch_latitude,
$FlightParametersStore.launch_longitude,
);
} else {
console.error("Invalid coordinate input");
// TODO: Show a validation error to the user.
// TODO: Show validation error to user
}
}
};
/**
* Updates the launch coordinates.
* @param {number} lat The new latitude.
* @param {number} lng The new longitude.
*/
export const updateLaunchPosition = (lat: number, lng: number) => {
async function handleGetPrediction() {
$FlightParametersStore.launch_datetime = `${startDate}T${startTime}Z`;
writeLocalStorage<FlightParameters>("flightParameters", $FlightParametersStore);
getForecast($FlightParametersStore).then(
(data) => {
console.log("Forecast request successful:", data);
addToast({
header: "Forecast Request",
body: "Forecast request successful!",
color: "success"
});
// Handle the response data as needed
}).catch((error) => {
console.error("Error getting forecast:", error);
addToast({
header: "Forecast Error",
body: `Error getting forecast: ${error.message}`,
color: "danger"
});
});
}
function toggleCollapse() {
isCollapsed = !isCollapsed;
}
// Exported functions for parent components
export function updateLaunchPosition(lat: number, lng: number) {
$FlightParametersStore.launch_latitude = lat;
$FlightParametersStore.launch_longitude = lng;
console.log("Launch position updated:", lat, lng);
inputLat = lat.toFixed(6).toString();
inputLng = lng.toFixed(6).toString();
inputLat = lat.toFixed(6);
inputLng = lng.toFixed(6);
setToCustomOnChange();
};
}
export const getElement = () => {
return element;
};
export function getSelectedProfile() {
return $FlightParametersStore.profile;
}
export const getSelectedProfile = () => {
return selectedProfile;
};
export function selectProfile(profile: ProfileName) {
$FlightParametersStore.profile = profile;
}
export const selectProfile = (profile: ProfileName) => {
selectedProfile = profile;
$FlightParametersStore.profile = PROFILE_MAP[selectedProfile];
console.log("Selected profile:", selectedProfile);
};
export const collapsePanel = () => {
export function collapsePanel() {
isCollapsed = true;
};
}
export const expandPanel = () => {
export function expandPanel() {
isCollapsed = false;
};
}
export const togglePanel = () => {
export function togglePanel() {
isCollapsed = !isCollapsed;
};
}
onMount(async () => {
const savedPoints = await getSavedPoints();
SavedPointsStore.set(savedPoints);
});
</script>
<Card>
<CardHeader
class="bg-primary text-white d-flex justify-content-between align-items-center card-header p-1 px-3"
@ -171,12 +205,11 @@
<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="Свернуть/развернуть параметры прогнозирования"
on:click={() => (isCollapsed = !isCollapsed)}
onclick={toggleCollapse}
>
<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={toggleCollapse}>
{#if isCollapsed}
<Icon name="caret-left-fill" class="text-white" />
{:else}
@ -185,6 +218,7 @@
</Button>
</button>
</CardHeader>
{#if !isCollapsed}
<CardBody>
<div class="d-flex gap-2">
@ -201,10 +235,10 @@
<FormGroup spacing="mb-2">
<Label for="flightProfile" class="form-label">Профиль полета:</Label>
<InputGroup size="sm">
<Input type="select" id="flightProfile" bind:value={selectedProfile}>
<Input type="select" id="flightProfile" bind:value={$FlightParametersStore.profile}>
<optgroup label="Стандартные профили">
{#each Object.keys(PROFILE_MAP) as profileName}
<option value={profileName}>{profileName}</option>
<option value={PROFILE_MAP[profileName as ProfileName]}>{profileName}</option>
{/each}
</optgroup>
<optgroup label="Пользовательские профили">
@ -215,7 +249,7 @@
color="secondary"
size="sm"
title="Edit profile"
disabled={selectedProfile !== "Custom"}
disabled={$FlightParametersStore.profile !== "custom_profile"}
>
<span>Редакт.</span>
<Icon name="gear-fill" />
@ -226,7 +260,7 @@
<FormGroup spacing="mb-2">
<Label for="startPoint" class="form-label">Точка старта:</Label>
<InputGroup size="sm">
<Input type="select" id="startPoint" bind:value={startPoint} on:change={setCoordinatesFromSavedPoint}>
<Input type="select" id="startPoint" bind:value={startPoint} onchange={setCoordinatesFromSavedPoint}>
<optgroup label="Сохраненные точки">
{#each $SavedPointsStore as point}
<option value={point.name} data-lat={point.lat} data-lng={point.lon} data-alt={point.alt}>
@ -241,7 +275,7 @@
<Button
color="secondary"
title="Edit Saved Locations"
on:click={handleClickPointListModal}
onclick={handleClickPointListModal}
>
<span>Редакт.</span>
<Icon name="journal-bookmark-fill" />
@ -252,12 +286,29 @@
<FormGroup spacing="mb-2">
<Label for="latitude" class="form-label">Широта/Долгота:</Label>
<InputGroup size="sm">
<Input id="latitude" type="text" bind:value={inputLat} placeholder="Latitude" on:change={setToCustomOnChange} />
<Input
id="latitude"
type="text"
bind:value={inputLat}
placeholder="Latitude"
onchange={setToCustomOnChange}
/>
<InputGroupText>/</InputGroupText>
<Input id="longitude" type="text" bind:value={inputLng} placeholder="Longitude" on:change={setToCustomOnChange} />
<Button color="success" size="sm" on:click={applyCoordinatesFromInput} title="Apply Coordinates"
>✓</Button
<Input
id="longitude"
type="text"
bind:value={inputLng}
placeholder="Longitude"
onchange={setToCustomOnChange}
/>
<Button
color="success"
size="sm"
onclick={applyCoordinatesFromInput}
title="Apply Coordinates"
>
</Button>
</InputGroup>
</FormGroup>
@ -266,8 +317,10 @@
color="outline-secondary"
size="sm"
class="w-100"
on:click={ handleClickSelectOnMap }>Указать на карте</Button
onclick={handleClickSelectOnMap}
>
Указать на карте
</Button>
</FormGroup>
<div class="d-flex gap-2">
@ -277,7 +330,7 @@
type="number"
id="startHeight"
class="form-control-sm"
on:change={setToCustomOnChange}
onchange={setToCustomOnChange}
bind:value={$FlightParametersStore.launch_altitude}
/>
</FormGroup>
@ -292,7 +345,7 @@
</FormGroup>
</div>
{#if selectedProfile != "Custom"}
{#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>
@ -317,7 +370,9 @@
<div class="d-grid gap-1">
<Button color="outline-secondary" size="sm">Показать график высоты</Button>
<Button size="sm" color="primary" on:click={handleGetPrediction}>Выполнить прогнозирование</Button>
<Button size="sm" color="primary" onclick={handleGetPrediction}>
Выполнить прогнозирование
</Button>
</div>
</CardBody>
{/if}

View file

@ -132,7 +132,7 @@
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
</div>
<div class="modal-body">
<div bind:this={table.element}>
<div bind:this={table.element} class="table-responsive">
<table class="table table-sm">
<thead>
<tr>

View file

@ -80,12 +80,10 @@ export const getForecast = async (
PredictionStore.set(parsePrediction(data.result.prediction) as Prediction);
writeLocalStorage("rawPrediction", data.result as RawPrediction);
writeLocalStorage("prediction", parsePrediction(data.result.prediction) as Prediction);
alert("Forecast request successful!");
// Handle the response data as needed
} catch (error) {
console.error("Error sending forecast request:", error);
alert("Error getting forecast: " + error);
return Promise.reject(new Error(`${error}`));
}
};

View file

@ -7,8 +7,18 @@ export const PROFILE_MAP = {
Custom: "custom_profile",
};
// Map of profile names to their string identifiers
export const PROFILE_NAMES = {
standard_profile: "Normal",
float_profile: "Float",
reverse_profile: "Reverse",
custom_profile: "Custom",
};
export type ProfileName = keyof typeof PROFILE_MAP;
export type ProfileIdentifier = keyof typeof PROFILE_NAMES;
export interface FlightParameters {
ascent_rate: number;
burst_altitude: number;