Compare commits
2 commits
87f0a53cb5
...
bb390d50dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb390d50dc | ||
|
|
329c1c2215 |
25 changed files with 1151 additions and 571 deletions
30
package-lock.json
generated
30
package-lock.json
generated
|
|
@ -9,10 +9,12 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltestrap/sveltestrap": "^7.1.0",
|
"@sveltestrap/sveltestrap": "^7.1.0",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"@types/leaflet": "^1.9.19",
|
||||||
|
"bootstrap-icons": "^1.13.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-velocity": "^2.1.4"
|
"leaflet-velocity": "^2.1.4",
|
||||||
|
"leaflet.heat": "^0.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^4.0.0",
|
"@sveltejs/adapter-auto": "^4.0.0",
|
||||||
|
|
@ -866,6 +868,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
|
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/leaflet": {
|
||||||
|
"version": "1.9.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.19.tgz",
|
||||||
|
"integrity": "sha512-pB+n2daHcZPF2FDaWa+6B0a0mSDf4dPU35y5iTXsx7x/PzzshiX5atYiS1jlBn43X7XvM8AP+AB26lnSk0J4GA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
|
|
@ -894,9 +909,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bootstrap-icons": {
|
"node_modules/bootstrap-icons": {
|
||||||
"version": "1.11.3",
|
"version": "1.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
|
||||||
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
|
"integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -1098,6 +1113,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/leaflet-velocity/-/leaflet-velocity-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet-velocity/-/leaflet-velocity-2.1.4.tgz",
|
||||||
"integrity": "sha512-uTmSb2/Kn28S0itlmJBMy2ZRKsisWUr2wm9rtkKXjpq9Sai7tqKdTRHKfLgTOgEdWFf5Ctt2bQoB7kb50qC7eg=="
|
"integrity": "sha512-uTmSb2/Kn28S0itlmJBMy2ZRKsisWUr2wm9rtkKXjpq9Sai7tqKdTRHKfLgTOgEdWFf5Ctt2bQoB7kb50qC7eg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet.heat": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet.heat/-/leaflet.heat-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cd5PbAA/rX3X3XKxfDoUGi9qp78FyhWYurFg3nsfhntcM/MCNK08pRkf4iEenO1KNqwVPKCmkyktjW3UD+h9bQ=="
|
||||||
|
},
|
||||||
"node_modules/locate-character": {
|
"node_modules/locate-character": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltestrap/sveltestrap": "^7.1.0",
|
"@sveltestrap/sveltestrap": "^7.1.0",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"@types/leaflet": "^1.9.19",
|
||||||
|
"bootstrap-icons": "^1.13.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-velocity": "^2.1.4"
|
"leaflet-velocity": "^2.1.4",
|
||||||
|
"leaflet.heat": "^0.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,15 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<link rel="stylesheet" href="%sveltekit.assets%/css/bootstrap.min.css">
|
<link rel="stylesheet" href="%sveltekit.assets%/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="%sveltekit.assets%/css/bootstrap-icons.css" />
|
||||||
|
<link rel="stylesheet" href="%sveltekit.assets%/ext/leaflet-ruler/leaflet-ruler.css" />
|
||||||
<link rel="stylesheet" href="%sveltekit.assets%/css/custom.css" />
|
<link rel="stylesheet" href="%sveltekit.assets%/css/custom.css" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css"
|
||||||
|
/>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
|
|
|
||||||
267
src/lib/components/ControlPanel.svelte
Normal file
267
src/lib/components/ControlPanel.svelte
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputGroupText,
|
||||||
|
Icon
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
|
import { getForecast } from "$lib/prediction";
|
||||||
|
import type { FlightParameters, ProfileName } from "$lib/types";
|
||||||
|
import { PROFILE_MAP } from "$lib/types";
|
||||||
|
import { FlightParametersStore, writeLocalStorage } from "$lib/stores";
|
||||||
|
|
||||||
|
let isCollapsed = false;
|
||||||
|
let selectedProfile: ProfileName = "Normal";
|
||||||
|
let startPoint = "Custom";
|
||||||
|
|
||||||
|
let element: HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
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 inputLat = $FlightParametersStore.launch_latitude.toString();
|
||||||
|
let inputLng = $FlightParametersStore.launch_longitude.toString();
|
||||||
|
|
||||||
|
$: $FlightParametersStore = {
|
||||||
|
...$FlightParametersStore,
|
||||||
|
profile: PROFILE_MAP[selectedProfile],
|
||||||
|
};
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
const 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.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the launch coordinates.
|
||||||
|
* @param {number} lat The new latitude.
|
||||||
|
* @param {number} lng The new longitude.
|
||||||
|
*/
|
||||||
|
export const 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();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getElement = () => {
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSelectedProfile = () => {
|
||||||
|
return selectedProfile;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectProfile = (profile: ProfileName) => {
|
||||||
|
selectedProfile = profile;
|
||||||
|
$FlightParametersStore.profile = PROFILE_MAP[selectedProfile];
|
||||||
|
console.log("Selected profile:", selectedProfile);
|
||||||
|
};
|
||||||
|
|
||||||
|
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="Свернуть/развернуть параметры прогнозирования"
|
||||||
|
on:click={() => (isCollapsed = !isCollapsed)}
|
||||||
|
>
|
||||||
|
<b class="card-title mb-0 text-white p-0">Параметры прогнозирования</b>
|
||||||
|
<Button class="p-0" size="sm" color="primary" on:click={() => (isCollapsed = !isCollapsed)}>
|
||||||
|
{#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>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||||
|
<Label for="startTime" class="form-label">Время старта (UTC):</Label>
|
||||||
|
<Input type="time" id="startTime" class="form-control-sm" bind:value={startTime} step="1" />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||||
|
<Label for="startDate" class="form-label">Дата старта:</Label>
|
||||||
|
<Input type="date" id="startDate" class="form-control-sm" bind:value={startDate} />
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormGroup spacing="mb-2">
|
||||||
|
<Label for="flightProfile" class="form-label">Профиль полета:</Label>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input type="select" id="flightProfile" bind:value={selectedProfile}>
|
||||||
|
<optgroup label="Стандартные профили">
|
||||||
|
{#each Object.keys(PROFILE_MAP) as profileName}
|
||||||
|
<option value={profileName}>{profileName}</option>
|
||||||
|
{/each}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Пользовательские профили">
|
||||||
|
<option>Custom</option>
|
||||||
|
</optgroup>
|
||||||
|
</Input>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
title="Edit profile"
|
||||||
|
disabled={selectedProfile !== "Custom"}
|
||||||
|
>
|
||||||
|
<span>Редакт.</span>
|
||||||
|
<Icon name="gear-fill" />
|
||||||
|
</Button>
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup spacing="mb-2">
|
||||||
|
<Label for="startPoint" class="form-label">Точка старта:</Label>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input type="select" id="startPoint" bind:value={startPoint}>
|
||||||
|
<option>Custom</option>
|
||||||
|
<option>Preset 1</option>
|
||||||
|
<option>Preset 2</option>
|
||||||
|
</Input>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
title="Edit Saved Locations"
|
||||||
|
on:click={() => console.log("Not implemented yet")}
|
||||||
|
>
|
||||||
|
<span>Редакт.</span>
|
||||||
|
<Icon name="journal-bookmark-fill" />
|
||||||
|
</Button>
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup spacing="mb-2">
|
||||||
|
<Label for="latitude" class="form-label">Широта/Долгота:</Label>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input id="latitude" type="text" bind:value={inputLat} placeholder="Latitude" />
|
||||||
|
<InputGroupText>/</InputGroupText>
|
||||||
|
<Input id="longitude" type="text" bind:value={inputLng} placeholder="Longitude" />
|
||||||
|
<Button color="success" size="sm" on:click={applyCoordinatesFromInput} title="Apply Coordinates"
|
||||||
|
>✓</Button
|
||||||
|
>
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup spacing="mb-2">
|
||||||
|
<Button
|
||||||
|
color="outline-secondary"
|
||||||
|
size="sm"
|
||||||
|
class="w-100"
|
||||||
|
on:click={ 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>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
id="startHeight"
|
||||||
|
class="form-control-sm"
|
||||||
|
bind:value={$FlightParametersStore.launch_altitude}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="d-grid gap-1">
|
||||||
|
<Button color="outline-secondary" size="sm">Показать график высоты</Button>
|
||||||
|
<Button size="sm" color="primary" on:click={handleGetPrediction}>Выполнить прогнозирование</Button>
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
|
@ -1,39 +1,43 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, createEventDispatcher } from "svelte";
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
|
import { ruler, Ruler } from "$lib/ext/leaflet-ruler/leaflet-ruler";
|
||||||
import type { Map as LeafletMap, LayerGroup } from "leaflet";
|
import type { Map as LeafletMap, LayerGroup } from "leaflet";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import WindVisualization from './WindVisualisation.svelte';
|
import WindVisualization from "$lib/components/WindVisualisation.svelte";
|
||||||
import { distHaversine } from "../lib/mathutil.ts";
|
import { distHaversine } from "$lib/mathutil";
|
||||||
import type { PredictionData, TelemetryData } from "../lib/types.ts";
|
import type { Prediction, Telemetry } from "$lib/types";
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {'prediction' | 'telemetry'}
|
|
||||||
*/
|
|
||||||
export let mode: "prediction" | "telemetry" = "prediction";
|
export let mode: "prediction" | "telemetry" = "prediction";
|
||||||
export let data: PredictionData | TelemetryData | null = null;
|
export let data: Prediction | Telemetry | null = null;
|
||||||
|
|
||||||
let map: typeof LeafletMap | undefined;
|
let map: LeafletMap;
|
||||||
let mapContainer: HTMLDivElement;
|
let mapContainer: HTMLDivElement;
|
||||||
let plotLayerGroup: typeof LayerGroup;
|
let plotLayerGroup: LayerGroup;
|
||||||
let mouseLat = 0;
|
let mouseLat = 0;
|
||||||
let mouseLng = 0;
|
let mouseLng = 0;
|
||||||
let isSelecting = false;
|
let isSelecting = false;
|
||||||
|
|
||||||
let windData;
|
let windData: any;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ coordinatesSelected: { lat: number; lng: number } }>();
|
const dispatch = createEventDispatcher<{ coordinatesSelected: { lat: number; lng: number } }>();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!mapContainer) return;
|
if (!mapContainer) return;
|
||||||
|
|
||||||
map = L.map(mapContainer).setView([51.505, -0.09], 13);
|
map = L.map(mapContainer, { zoomControl: false }).setView([51.505, -0.09], 13);
|
||||||
|
L.control.zoom({ position: "bottomleft" }).addTo(map);
|
||||||
|
|
||||||
plotLayerGroup = L.layerGroup().addTo(map);
|
plotLayerGroup = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
|
ruler({
|
||||||
|
position: "bottomright",
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
const response = await fetch("src/routes/testVelo.json");
|
const response = await fetch("src/routes/testVelo.json");
|
||||||
windData = await response.json();
|
windData = await response.json();
|
||||||
|
|
||||||
|
|
@ -66,11 +70,11 @@
|
||||||
if (mapContainer) mapContainer.style.cursor = "";
|
if (mapContainer) mapContainer.style.cursor = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const plotData = (plotData: PredictionData | TelemetryData) => {
|
export const plotData = (plotData: Prediction | Telemetry) => {
|
||||||
if (mode === "prediction") {
|
if (mode === "prediction") {
|
||||||
plotPrediction(plotData as PredictionData);
|
plotPrediction(plotData as Prediction);
|
||||||
} else if (mode === "telemetry") {
|
} else if (mode === "telemetry") {
|
||||||
plotTelemetry(plotData as TelemetryData);
|
plotTelemetry(plotData as Telemetry);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -83,7 +87,7 @@
|
||||||
const burstIcon = L.icon({ iconUrl: "pop-marker.png", iconSize: [16, 16], iconAnchor: [8, 8] });
|
const burstIcon = L.icon({ iconUrl: "pop-marker.png", iconSize: [16, 16], iconAnchor: [8, 8] });
|
||||||
const telemetryIcon = L.icon({ iconUrl: "marker-sm-red.png", iconSize: [10, 10], iconAnchor: [5, 5] });
|
const telemetryIcon = L.icon({ iconUrl: "marker-sm-red.png", iconSize: [10, 10], iconAnchor: [5, 5] });
|
||||||
|
|
||||||
const plotPrediction = (prediction: PredictionData) => {
|
const plotPrediction = (prediction: Prediction) => {
|
||||||
const { launch, landing, burst, flight_path, flight_time } = prediction;
|
const { launch, landing, burst, flight_path, flight_time } = prediction;
|
||||||
|
|
||||||
const range = distHaversine(launch.latlng, landing.latlng, 1);
|
const range = distHaversine(launch.latlng, landing.latlng, 1);
|
||||||
|
|
@ -100,7 +104,7 @@
|
||||||
map?.fitBounds(L.latLngBounds(flight_path));
|
map?.fitBounds(L.latLngBounds(flight_path));
|
||||||
};
|
};
|
||||||
|
|
||||||
const plotTelemetry = (telemetry: TelemetryData) => {
|
const plotTelemetry = (telemetry: Telemetry) => {
|
||||||
L.marker(telemetry.launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
|
L.marker(telemetry.launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
|
||||||
|
|
||||||
telemetry.datapoints.forEach((point) => {
|
telemetry.datapoints.forEach((point) => {
|
||||||
102
src/lib/components/Navbar.svelte
Normal file
102
src/lib/components/Navbar.svelte
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { checkAuthenticated, logout } from '$lib/auth';
|
||||||
|
import {
|
||||||
|
Collapse,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownToggle,
|
||||||
|
Nav,
|
||||||
|
NavItem,
|
||||||
|
NavLink,
|
||||||
|
Navbar,
|
||||||
|
NavbarBrand,
|
||||||
|
NavbarToggler
|
||||||
|
} from '@sveltestrap/sveltestrap';
|
||||||
|
|
||||||
|
// State for the navbar toggler
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
// Check if user is authenticated (using localStorage token)
|
||||||
|
let isAuthenticated = false;
|
||||||
|
|
||||||
|
// This should be reactive to changes in auth status
|
||||||
|
$: if (typeof window !== 'undefined') {
|
||||||
|
Promise.resolve(checkAuthenticated()).then((result) => {
|
||||||
|
isAuthenticated = result;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isAuthenticated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
// Clear authentication tokens
|
||||||
|
try {
|
||||||
|
logout();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout failed:', error);
|
||||||
|
}
|
||||||
|
// Update auth status
|
||||||
|
isAuthenticated = false;
|
||||||
|
|
||||||
|
// Redirect to login page
|
||||||
|
goto('/');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<Navbar color="light" light expand="lg" fixed="top" class="custom-navbar border-bottom">
|
||||||
|
<NavbarBrand href="/" class="nav-full-height">
|
||||||
|
<img src="/logo.svg" alt="Logo" height="34" class="d-inline-block align-text-top" />
|
||||||
|
</NavbarBrand>
|
||||||
|
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
|
||||||
|
<div class="navbar-collapse collapse" class:show={isOpen} id="navbarContent">
|
||||||
|
<Nav class="me-auto mb-lg-0" navbar>
|
||||||
|
<NavItem>
|
||||||
|
<NavLink
|
||||||
|
href="/predict"
|
||||||
|
class="nav-full-height border border-top-0"
|
||||||
|
active={$page.url.pathname === '/predict'}>
|
||||||
|
Прогнозирование
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
<NavItem>
|
||||||
|
<NavLink
|
||||||
|
href="/track"
|
||||||
|
class="nav-full-height border border-top-0"
|
||||||
|
active={$page.url.pathname === '/track'}>
|
||||||
|
Слежение
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
</Nav>
|
||||||
|
<Nav navbar>
|
||||||
|
{#if isAuthenticated}
|
||||||
|
<Dropdown nav inNavbar>
|
||||||
|
<DropdownToggle nav caret class="nav-full-height border border-top-0">
|
||||||
|
Account
|
||||||
|
</DropdownToggle>
|
||||||
|
<DropdownMenu end>
|
||||||
|
<DropdownItem href="/user/account">Account Settings</DropdownItem>
|
||||||
|
<DropdownItem href="/user/templates">Saved Templates</DropdownItem>
|
||||||
|
<DropdownItem href="/user/predictions">Prediction History</DropdownItem>
|
||||||
|
<DropdownItem href="/user/flights">Flight History</DropdownItem>
|
||||||
|
<DropdownItem divider />
|
||||||
|
<DropdownItem on:click={handleLogout}>Logout</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
{:else}
|
||||||
|
<NavItem>
|
||||||
|
<NavLink
|
||||||
|
href="/login"
|
||||||
|
class="nav-full-height border border-top-0"
|
||||||
|
active={$page.url.pathname === '/login'}>
|
||||||
|
Login
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
{/if}
|
||||||
|
</Nav>
|
||||||
|
</div>
|
||||||
|
</Navbar>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
11
src/lib/components/PanelContainer.svelte
Normal file
11
src/lib/components/PanelContainer.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let element: HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
export function getElement() {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={element} class="panel-container">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
115
src/lib/components/ScenarioPanel.svelte
Normal file
115
src/lib/components/ScenarioPanel.svelte
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputGroupText,
|
||||||
|
Icon,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
|
import { PROFILE_MAP } from "$lib/types";
|
||||||
|
import { FlightParametersStore, writeLocalStorage } from "$lib/stores";
|
||||||
|
|
||||||
|
let isCollapsed = 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="Свернуть/развернуть параметры прогнозирования"
|
||||||
|
on:click={() => (isCollapsed = !isCollapsed)}
|
||||||
|
>
|
||||||
|
<b class="card-title mb-0 text-white p-0">Сценарий прогнозирования</b>
|
||||||
|
<Button class="p-0" size="sm" color="primary" on:click={() => (isCollapsed = !isCollapsed)}>
|
||||||
|
{#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">Название сценария:</Label>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input id="scenarioName" type="text" />
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 mb-2">
|
||||||
|
<Button class="flex-fill" color="secondary" size="sm">
|
||||||
|
Сохранить
|
||||||
|
<Icon name="save" />
|
||||||
|
</Button>
|
||||||
|
<Button class="flex-fill" color="secondary" size="sm">
|
||||||
|
Загрузить
|
||||||
|
<Icon name="folder2-open" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
class="mb-2 w-100"
|
||||||
|
>
|
||||||
|
Редактировать сохраненные сценарии
|
||||||
|
<Icon name="journal-bookmark-fill" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<FormGroup spacing="mb-2">
|
||||||
|
<Label for="scenarioMode" class="form-label">Режим сценария:</Label>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input type="select" id="scenarioMode">
|
||||||
|
<option>Обычный</option>
|
||||||
|
<option>Почасовой</option>
|
||||||
|
<option>Ансамблевый</option>
|
||||||
|
</Input>
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
on:click={() => console.log("Not implemented yet")}
|
||||||
|
>
|
||||||
|
<span>Экспорт</span>
|
||||||
|
<Icon name="file-earmark-arrow-down" />
|
||||||
|
</Button>
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
</CardBody>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
50
src/lib/components/TabComponent.svelte
Normal file
50
src/lib/components/TabComponent.svelte
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Icon } from '@sveltestrap/sveltestrap';
|
||||||
|
|
||||||
|
type Tab = {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** An array of tab objects to display. */
|
||||||
|
export let tabs: Tab[] = [];
|
||||||
|
|
||||||
|
/** The id of the currently active tab. Use `bind:activeTab` for two-way binding. */
|
||||||
|
export let activeTab: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-start mb-1 gap-1">
|
||||||
|
{#each tabs as tab (tab.id)}
|
||||||
|
<button
|
||||||
|
class="custom-tab btn btn-outline-primary d-flex flex-column align-items-center justify-content-center px-1"
|
||||||
|
class:active={activeTab === tab.id}
|
||||||
|
on:click={() => (activeTab = tab.id)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon name={tab.icon} class="custom-tab-icon" />
|
||||||
|
<span class="custom-tab-label">{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom-tab {
|
||||||
|
width: 4.5rem;
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tab.active {
|
||||||
|
background-color: var(--bs-primary) !important;
|
||||||
|
color: var(--bs-btn-active-color);
|
||||||
|
}
|
||||||
|
.custom-tab:hover {
|
||||||
|
background-color: var(--bs-primary) !important;
|
||||||
|
color: var(--bs-btn-active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tab-label {
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
97
src/lib/components/TelemetryPanel.svelte
Normal file
97
src/lib/components/TelemetryPanel.svelte
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
} from '@sveltestrap/sveltestrap';
|
||||||
|
//import { telemetryStore } from '../lib/telemetry.ts'; // Import your telemetry store
|
||||||
|
|
||||||
|
let telemetry: { latitude?: number; longitude?: number; altitude?: number } = {};
|
||||||
|
let isCollapsed = false;
|
||||||
|
|
||||||
|
// Subscribe to the telemetry store
|
||||||
|
//const unsubscribe = telemetryStore.subscribe((data) => {
|
||||||
|
// telemetry = data;
|
||||||
|
//});
|
||||||
|
|
||||||
|
telemetry = {
|
||||||
|
latitude: 56.3576,
|
||||||
|
longitude: 39.8666,
|
||||||
|
altitude: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// onMount(() => {
|
||||||
|
// return () => {
|
||||||
|
// unsubscribe(); // Cleanup subscription on component destroy
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
class="bg-primary text-white d-flex justify-content-between align-items-center p-1 px-3"
|
||||||
|
style="cursor:pointer;"
|
||||||
|
>
|
||||||
|
<b class="card-title mb-0 p-0">Последние данные телеметрии</b>
|
||||||
|
<Button class="p-0" size="sm" color="primary" on:click={() => (isCollapsed = !isCollapsed)}>
|
||||||
|
{#if isCollapsed}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="bi bi-caret-left-fill"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m3.86 8.753 5.482 4.796c.646.566 1.658.106 1.658-.753V3.204a1 1 0 0 0-1.659-.753l-5.48 4.796a1 1 0 0 0 0 1.506z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="bi bi-caret-down"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
{#if !isCollapsed}
|
||||||
|
<CardBody>
|
||||||
|
<FormGroup spacing="mb-2">
|
||||||
|
<Label class="small">Широта:</Label>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input type="text" value={telemetry.latitude || 'N/A'} readonly />
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup spacing="mb-2">
|
||||||
|
<Label class="small">Долгота:</Label>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input type="text" value={telemetry.longitude || 'N/A'} readonly />
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup spacing="mb-2">
|
||||||
|
<Label class="small">Высота (м):</Label>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input type="text" value={telemetry.altitude || 'N/A'} readonly />
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
</CardBody>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@
|
||||||
displayValues: true,
|
displayValues: true,
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
velocityType: 'Wind Speed',
|
velocityType: 'Wind Speed',
|
||||||
position: 'bottomleft',
|
position: 'bottomright',
|
||||||
emptyString: 'No wind data',
|
emptyString: 'No wind data',
|
||||||
},
|
},
|
||||||
data: windData
|
data: windData
|
||||||
286
src/lib/ext/leaflet-ruler/leaflet-ruler.ts
Normal file
286
src/lib/ext/leaflet-ruler/leaflet-ruler.ts
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
import * as L from "leaflet";
|
||||||
|
import { distHaversine, bearingHaversine } from "$lib/mathutil";
|
||||||
|
|
||||||
|
// Define an interface for the control's options for type safety.
|
||||||
|
export interface RulerOptions extends L.ControlOptions {
|
||||||
|
events?: {
|
||||||
|
onToggle?: (isActive: boolean) => void;
|
||||||
|
};
|
||||||
|
circleMarker?: L.CircleMarkerOptions;
|
||||||
|
lineStyle?: L.PolylineOptions;
|
||||||
|
lengthUnit?: {
|
||||||
|
display?: string;
|
||||||
|
decimal?: number;
|
||||||
|
factor?: number | null;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
angleUnit?: {
|
||||||
|
display?: string;
|
||||||
|
decimal?: number;
|
||||||
|
factor?: number | null;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define an interface for the measurement result.
|
||||||
|
interface MeasurementResult {
|
||||||
|
Bearing: number;
|
||||||
|
Distance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a modern TypeScript class that extends L.Control.
|
||||||
|
export class Ruler extends L.Control {
|
||||||
|
// Override the default options with our custom ones.
|
||||||
|
public options: RulerOptions = {
|
||||||
|
position: "topright",
|
||||||
|
events: {
|
||||||
|
onToggle: () => {},
|
||||||
|
},
|
||||||
|
circleMarker: {
|
||||||
|
color: "red",
|
||||||
|
radius: 2,
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: "red",
|
||||||
|
dashArray: "1,6",
|
||||||
|
},
|
||||||
|
lengthUnit: {
|
||||||
|
display: "km",
|
||||||
|
decimal: 2,
|
||||||
|
factor: null,
|
||||||
|
label: "Distance:",
|
||||||
|
},
|
||||||
|
angleUnit: {
|
||||||
|
display: "°",
|
||||||
|
decimal: 2,
|
||||||
|
factor: null,
|
||||||
|
label: "Bearing:",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Declare class properties with types.
|
||||||
|
private _lastClickTime = 0;
|
||||||
|
private _map?: L.Map;
|
||||||
|
private _container?: HTMLElement;
|
||||||
|
private _choice = false;
|
||||||
|
private _defaultCursor = "";
|
||||||
|
private _allLayers: L.LayerGroup = L.layerGroup();
|
||||||
|
private _clickedLatLong: L.LatLng | null = null;
|
||||||
|
private _clickedPoints: L.LatLng[] = [];
|
||||||
|
private _totalLength = 0;
|
||||||
|
private _clickCount = 0;
|
||||||
|
private _tempLine: L.FeatureGroup = L.featureGroup();
|
||||||
|
private _tempPoint: L.FeatureGroup = L.featureGroup();
|
||||||
|
private _pointLayer: L.FeatureGroup = L.featureGroup();
|
||||||
|
private _polylineLayer: L.FeatureGroup = L.featureGroup();
|
||||||
|
private _movingLatLong: L.LatLng | null = null;
|
||||||
|
private _result: MeasurementResult = { Bearing: 0, Distance: 0 };
|
||||||
|
private _addedLength = 0;
|
||||||
|
|
||||||
|
constructor(options?: RulerOptions) {
|
||||||
|
super(options);
|
||||||
|
L.Util.setOptions(this, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActive(): boolean {
|
||||||
|
return this._choice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onAdd(map: L.Map): HTMLElement {
|
||||||
|
this._map = map;
|
||||||
|
this._container = L.DomUtil.create("div", "leaflet-bar leaflet-ruler");
|
||||||
|
L.DomEvent.disableClickPropagation(this._container);
|
||||||
|
L.DomEvent.on(this._container, "click", this._toggleMeasure, this);
|
||||||
|
this._defaultCursor = this._map.getContainer().style.cursor;
|
||||||
|
this._allLayers = L.layerGroup();
|
||||||
|
return this._container;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onRemove(): void {
|
||||||
|
if (this._container) {
|
||||||
|
L.DomEvent.off(this._container, "click", this._toggleMeasure, this);
|
||||||
|
}
|
||||||
|
if (this._choice) {
|
||||||
|
this._toggleMeasure(); // Turn off measurements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleMeasure(): void {
|
||||||
|
this._choice = !this._choice;
|
||||||
|
this.options.events?.onToggle?.(this._choice);
|
||||||
|
|
||||||
|
this._clickedLatLong = null;
|
||||||
|
this._clickedPoints = [];
|
||||||
|
this._totalLength = 0;
|
||||||
|
|
||||||
|
if (!this._map || !this._container) return;
|
||||||
|
|
||||||
|
const mapContainer = this._map.getContainer();
|
||||||
|
|
||||||
|
if (this._choice) {
|
||||||
|
this._map.doubleClickZoom.disable();
|
||||||
|
L.DomEvent.on(mapContainer, "keydown", this._escape, this);
|
||||||
|
L.DomEvent.on(mapContainer, "dblclick", this._closePath, this);
|
||||||
|
this._container.classList.add("leaflet-ruler-clicked");
|
||||||
|
this._clickCount = 0;
|
||||||
|
this._tempLine = L.featureGroup().addTo(this._allLayers);
|
||||||
|
this._tempPoint = L.featureGroup().addTo(this._allLayers);
|
||||||
|
this._pointLayer = L.featureGroup().addTo(this._allLayers);
|
||||||
|
this._polylineLayer = L.featureGroup().addTo(this._allLayers);
|
||||||
|
this._allLayers.addTo(this._map);
|
||||||
|
mapContainer.style.cursor = "crosshair";
|
||||||
|
this._map.on("click", this._clicked, this);
|
||||||
|
this._map.on("mousemove", this._moving, this);
|
||||||
|
} else {
|
||||||
|
this._map.doubleClickZoom.enable();
|
||||||
|
L.DomEvent.off(mapContainer, "keydown", this._escape, this);
|
||||||
|
L.DomEvent.off(mapContainer, "dblclick", this._closePath, this);
|
||||||
|
this._container.classList.remove("leaflet-ruler-clicked");
|
||||||
|
this._map.removeLayer(this._allLayers);
|
||||||
|
this._allLayers = L.layerGroup();
|
||||||
|
mapContainer.style.cursor = this._defaultCursor;
|
||||||
|
this._map.off("click", this._clicked, this);
|
||||||
|
this._map.off("mousemove", this._moving, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clicked(e: L.LeafletMouseEvent): void {
|
||||||
|
// hack to prevent adding the same point twice on double click
|
||||||
|
let clickTime = Date.now();
|
||||||
|
if (clickTime - this._lastClickTime < 200) {
|
||||||
|
this._closePath();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastClickTime = clickTime;
|
||||||
|
|
||||||
|
this._clickedLatLong = e.latlng;
|
||||||
|
this._clickedPoints.push(this._clickedLatLong);
|
||||||
|
L.circleMarker(this._clickedLatLong, this.options.circleMarker).addTo(this._pointLayer);
|
||||||
|
|
||||||
|
if (this._clickCount > 0 && !e.latlng.equals(this._clickedPoints[this._clickedPoints.length - 2], 0.0001)) {
|
||||||
|
if (this._movingLatLong) {
|
||||||
|
L.polyline(
|
||||||
|
[this._clickedPoints[this._clickCount - 1], this._movingLatLong],
|
||||||
|
this.options.lineStyle
|
||||||
|
).addTo(this._polylineLayer);
|
||||||
|
}
|
||||||
|
let text: string;
|
||||||
|
this._totalLength += this._result.Distance;
|
||||||
|
const angleUnit = this.options.angleUnit!;
|
||||||
|
const lengthUnit = this.options.lengthUnit!;
|
||||||
|
|
||||||
|
if (this._clickCount > 1) {
|
||||||
|
text = `<b>${angleUnit.label}</b> ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
|
||||||
|
angleUnit.display
|
||||||
|
}<br><b>${lengthUnit.label}</b> ${this._totalLength.toFixed(lengthUnit.decimal)} ${
|
||||||
|
lengthUnit.display
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
text = `<b>${angleUnit.label}</b> ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
|
||||||
|
angleUnit.display
|
||||||
|
}<br><b>${lengthUnit.label}</b> ${this._result.Distance.toFixed(lengthUnit.decimal)} ${
|
||||||
|
lengthUnit.display
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
L.circleMarker(this._clickedLatLong, this.options.circleMarker)
|
||||||
|
.bindTooltip(text, { permanent: true, className: "result-tooltip" })
|
||||||
|
.addTo(this._pointLayer)
|
||||||
|
.openTooltip();
|
||||||
|
}
|
||||||
|
this._clickCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _moving(e: L.LeafletMouseEvent): void {
|
||||||
|
if (this._clickedLatLong && this._map) {
|
||||||
|
this._movingLatLong = e.latlng;
|
||||||
|
|
||||||
|
this._tempLine.clearLayers();
|
||||||
|
this._tempPoint.clearLayers();
|
||||||
|
|
||||||
|
this._calculateBearingAndDistance();
|
||||||
|
this._addedLength = this._result.Distance + this._totalLength;
|
||||||
|
|
||||||
|
L.polyline([this._clickedLatLong, this._movingLatLong], this.options.lineStyle).addTo(this._tempLine);
|
||||||
|
|
||||||
|
const angleUnit = this.options.angleUnit!;
|
||||||
|
const lengthUnit = this.options.lengthUnit!;
|
||||||
|
let text: string;
|
||||||
|
|
||||||
|
if (this._clickCount > 1) {
|
||||||
|
text = `<b>${angleUnit.label}</b> ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
|
||||||
|
angleUnit.display
|
||||||
|
}<br><b>${lengthUnit.label}</b> ${this._addedLength.toFixed(lengthUnit.decimal)} ${
|
||||||
|
lengthUnit.display
|
||||||
|
}<br><div class="plus-length">(+${this._result.Distance.toFixed(lengthUnit.decimal)})</div>`;
|
||||||
|
} else {
|
||||||
|
text = `<b>${angleUnit.label}</b> ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
|
||||||
|
angleUnit.display
|
||||||
|
}<br><b>${lengthUnit.label}</b> ${this._result.Distance.toFixed(lengthUnit.decimal)} ${
|
||||||
|
lengthUnit.display
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
L.circleMarker(this._movingLatLong, this.options.circleMarker)
|
||||||
|
.bindTooltip(text, { sticky: true, offset: L.point(0, -40), className: "moving-tooltip" })
|
||||||
|
.addTo(this._tempPoint)
|
||||||
|
.openTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _escape(e: Event): void {
|
||||||
|
if ((e as KeyboardEvent).key === "Escape") {
|
||||||
|
if (this._clickCount > 0) {
|
||||||
|
this._closePath();
|
||||||
|
} else {
|
||||||
|
this._toggleMeasure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _calculateBearingAndDistance(): void {
|
||||||
|
if (!this._clickedLatLong || !this._movingLatLong) return;
|
||||||
|
|
||||||
|
const f1 = this._clickedLatLong.lat;
|
||||||
|
const l1 = this._clickedLatLong.lng;
|
||||||
|
const f2 = this._movingLatLong.lat;
|
||||||
|
const l2 = this._movingLatLong.lng;
|
||||||
|
|
||||||
|
const angleUnit = this.options.angleUnit!;
|
||||||
|
const lengthUnit = this.options.lengthUnit!;
|
||||||
|
|
||||||
|
const brng = bearingHaversine({ lat: f1, lng: l1 }, { lat: f2, lng: l2 });
|
||||||
|
|
||||||
|
const distance = distHaversine({ lat: f1, lng: l1 }, { lat: f2, lng: l2 });
|
||||||
|
|
||||||
|
if (angleUnit.factor) {
|
||||||
|
this._result.Bearing = brng * angleUnit.factor;
|
||||||
|
} else {
|
||||||
|
this._result.Bearing = brng;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lengthUnit.factor) {
|
||||||
|
this._result.Distance = distance * lengthUnit.factor;
|
||||||
|
} else {
|
||||||
|
this._result.Distance = distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._result = {
|
||||||
|
Bearing: brng,
|
||||||
|
Distance: distance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _closePath(): void {
|
||||||
|
if (!this._map || !this._container) return;
|
||||||
|
|
||||||
|
this._map.removeLayer(this._tempLine);
|
||||||
|
this._map.removeLayer(this._tempPoint);
|
||||||
|
this._choice = false;
|
||||||
|
this._toggleMeasure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory function for creating the control, maintaining the Leaflet convention.
|
||||||
|
export const ruler = (options?: RulerOptions) => {
|
||||||
|
return new Ruler(options);
|
||||||
|
};
|
||||||
|
|
@ -2,7 +2,7 @@ export function distHaversine(
|
||||||
p1: { lat: number; lng: number },
|
p1: { lat: number; lng: number },
|
||||||
p2: { lat: number; lng: number },
|
p2: { lat: number; lng: number },
|
||||||
precision?: number
|
precision?: number
|
||||||
): string {
|
): number {
|
||||||
const R = 6371; // Earth's mean radius in km
|
const R = 6371; // Earth's mean radius in km
|
||||||
|
|
||||||
const rad = (x: number): number => (x * Math.PI) / 180;
|
const rad = (x: number): number => (x * Math.PI) / 180;
|
||||||
|
|
@ -20,5 +20,20 @@ export function distHaversine(
|
||||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
const d = R * c;
|
const d = R * c;
|
||||||
|
|
||||||
return d.toFixed(precision ?? 3);
|
return precision ? parseFloat(d.toFixed(precision)) : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bearingHaversine(
|
||||||
|
p1: { lat: number; lng: number },
|
||||||
|
p2: { lat: number; lng: number }
|
||||||
|
): number {
|
||||||
|
const rad = (x: number): number => (x * Math.PI) / 180;
|
||||||
|
|
||||||
|
const dLong = rad(p2.lng - p1.lng);
|
||||||
|
const y = Math.sin(dLong) * Math.cos(rad(p2.lat));
|
||||||
|
const x =
|
||||||
|
Math.cos(rad(p1.lat)) * Math.sin(rad(p2.lat)) -
|
||||||
|
Math.sin(rad(p1.lat)) * Math.cos(rad(p2.lat)) * Math.cos(dLong);
|
||||||
|
|
||||||
|
return (Math.atan2(y, x) * 180) / Math.PI;
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,11 @@ export interface FlightParameters {
|
||||||
version: number;
|
version: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
latlng: LatLngLiteral & { alt: number };
|
||||||
|
datetime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TelemetryPoint {
|
export interface TelemetryPoint {
|
||||||
altitude: number;
|
altitude: number;
|
||||||
datetime: string;
|
datetime: string;
|
||||||
|
|
@ -42,11 +47,8 @@ export interface RawTelemetry {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Telemetry {
|
export interface Telemetry {
|
||||||
flight_path: [number, number, number][];
|
flight_path: LatLngExpression[];
|
||||||
launch: {
|
launch: Point;
|
||||||
latlng: LatLngExpression;
|
|
||||||
datetime: Date;
|
|
||||||
};
|
|
||||||
datapoints: TelemetryPoint[];
|
datapoints: TelemetryPoint[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,38 +76,10 @@ export interface RawPrediction {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Prediction {
|
export interface Prediction {
|
||||||
flight_path: [number, number, number][];
|
flight_path: LatLngExpression[];
|
||||||
launch: {
|
launch: Point;
|
||||||
latlng: LatLngExpression;
|
burst: Point;
|
||||||
datetime: Date;
|
landing: Point;
|
||||||
};
|
|
||||||
burst: {
|
|
||||||
latlng: LatLngExpression;
|
|
||||||
datetime: Date;
|
|
||||||
};
|
|
||||||
landing: {
|
|
||||||
latlng: LatLngExpression;
|
|
||||||
datetime: Date;
|
|
||||||
};
|
|
||||||
profile: string;
|
profile: string;
|
||||||
flight_time: number;
|
flight_time: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Point {
|
|
||||||
latlng: LatLngLiteral & { alt: number };
|
|
||||||
datetime: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PredictionData {
|
|
||||||
launch: Point;
|
|
||||||
landing: Point;
|
|
||||||
burst: Point;
|
|
||||||
flight_path: LatLngExpression[];
|
|
||||||
flight_time: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TelemetryData {
|
|
||||||
launch: Point;
|
|
||||||
datapoints: TelemetryPoint[];
|
|
||||||
flight_path: LatLngExpression[];
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import Navbar from './Navbar.svelte';
|
import Navbar from '$lib/components/Navbar.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardBody,
|
|
||||||
Button,
|
|
||||||
FormGroup,
|
|
||||||
Label,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputGroupText,
|
|
||||||
} from "@sveltestrap/sveltestrap";
|
|
||||||
|
|
||||||
import { getForecast } from "$lib/prediction";
|
|
||||||
import type { FlightParameters, ProfileName } from "$lib/types";
|
|
||||||
import { PROFILE_MAP } from "$lib/types";
|
|
||||||
import { FlightParametersStore, writeLocalStorage } from "$lib/stores";
|
|
||||||
|
|
||||||
let isCollapsed = false;
|
|
||||||
let selectedProfile: ProfileName = "Normal";
|
|
||||||
let startPoint = "Custom";
|
|
||||||
|
|
||||||
let element: HTMLDivElement | null = null;
|
|
||||||
|
|
||||||
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 inputLat = $FlightParametersStore.launch_latitude.toString();
|
|
||||||
let inputLng = $FlightParametersStore.launch_longitude.toString();
|
|
||||||
|
|
||||||
$: $FlightParametersStore = {
|
|
||||||
...$FlightParametersStore,
|
|
||||||
profile: PROFILE_MAP[selectedProfile],
|
|
||||||
};
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
const 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.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the launch coordinates.
|
|
||||||
* @param {number} lat The new latitude.
|
|
||||||
* @param {number} lng The new longitude.
|
|
||||||
*/
|
|
||||||
export const 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();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getElement = () => {
|
|
||||||
return element;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={element}
|
|
||||||
style="width: 23rem; max-height: 80vh; overflow-y: auto; z-index: 1000;"
|
|
||||||
class="position-absolute shadow-lg panel-container"
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
class="bg-primary text-white d-flex justify-content-between align-items-center card-header"
|
|
||||||
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="Свернуть/развернуть параметры прогнозирования"
|
|
||||||
on:click={() => (isCollapsed = !isCollapsed)}
|
|
||||||
>
|
|
||||||
<h6 class="card-title mb-0 text-white">Параметры прогнозирования</h6>
|
|
||||||
<Button size="sm" color="primary" on:click={() => (isCollapsed = !isCollapsed)}>
|
|
||||||
{#if isCollapsed}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="bi bi-caret-left-fill"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="m3.86 8.753 5.482 4.796c.646.566 1.658.106 1.658-.753V3.204a1 1 0 0 0-1.659-.753l-5.48 4.796a1 1 0 0 0 0 1.506z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="bi bi-caret-down"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</button>
|
|
||||||
</CardHeader>
|
|
||||||
{#if !isCollapsed}
|
|
||||||
<CardBody>
|
|
||||||
<FormGroup spacing="mb-2">
|
|
||||||
<Label for="flightProfile" class="form-label">Профиль полета:</Label>
|
|
||||||
<InputGroup size="sm">
|
|
||||||
<Input type="select" id="flightProfile" bind:value={selectedProfile}>
|
|
||||||
{#each Object.keys(PROFILE_MAP) as profileName}
|
|
||||||
<option value={profileName}>{profileName}</option>
|
|
||||||
{/each}
|
|
||||||
</Input>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
size="sm"
|
|
||||||
title="Edit profile"
|
|
||||||
disabled={selectedProfile !== "Custom"}
|
|
||||||
>
|
|
||||||
<span>Редакт.</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
fill="currentColor"
|
|
||||||
class="bi bi-gear-fill"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413-1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
</InputGroup>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup spacing="mb-2">
|
|
||||||
<Label for="startPoint" class="form-label">Точка старта:</Label>
|
|
||||||
<InputGroup size="sm">
|
|
||||||
<Input type="select" id="startPoint" bind:value={startPoint}>
|
|
||||||
<option>Custom</option>
|
|
||||||
<option>Preset 1</option>
|
|
||||||
<option>Preset 2</option>
|
|
||||||
</Input>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
title="Edit Saved Locations"
|
|
||||||
on:click={() => console.log("Not implemented yet")}
|
|
||||||
>
|
|
||||||
<span>Редакт.</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="bi bi-journal-bookmark-fill"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M6 1h6v7a.5.5 0 0 1-.757.429L9 7.083 6.757 8.43A.5.5 0 0 1 6 8z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
</InputGroup>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup spacing="mb-2">
|
|
||||||
<Label for="latitude" class="form-label">Широта/Долгота:</Label>
|
|
||||||
<InputGroup size="sm">
|
|
||||||
<Input id="latitude" type="text" bind:value={inputLat} placeholder="Latitude" />
|
|
||||||
<InputGroupText>/</InputGroupText>
|
|
||||||
<Input id="longitude" type="text" bind:value={inputLng} placeholder="Longitude" />
|
|
||||||
<Button color="success" size="sm" on:click={applyCoordinatesFromInput} title="Apply Coordinates"
|
|
||||||
>✓</Button
|
|
||||||
>
|
|
||||||
</InputGroup>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup spacing="mb-2">
|
|
||||||
<Button
|
|
||||||
color="outline-secondary"
|
|
||||||
size="sm"
|
|
||||||
class="w-100"
|
|
||||||
on:click={ handleClickSelectOnMap }>Указать на карте</Button
|
|
||||||
>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup spacing="mb-2">
|
|
||||||
<Label for="startHeight" class="form-label">Высота точки старта:</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
id="startHeight"
|
|
||||||
class="form-control-sm"
|
|
||||||
bind:value={$FlightParametersStore.launch_altitude}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<div class="mb-2 d-flex gap-2">
|
|
||||||
<FormGroup class="flex-fill" spacing="mb-0">
|
|
||||||
<Label for="startTime" class="form-label">Время старта (UTC):</Label>
|
|
||||||
<Input type="time" id="startTime" class="form-control-sm" bind:value={startTime} step="1" />
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup class="flex-fill" spacing="mb-0">
|
|
||||||
<Label for="startDate" class="form-label">Дата старта:</Label>
|
|
||||||
<Input type="date" id="startDate" class="form-control-sm" bind:value={startDate} />
|
|
||||||
</FormGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-2 d-flex gap-2">
|
|
||||||
<FormGroup class="flex-fill" spacing="mb-0">
|
|
||||||
<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" spacing="mb-0">
|
|
||||||
<Label for="descentRate" class="form-label">Скорость спуска (м/с):</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
id="descentRate"
|
|
||||||
class="form-control-sm"
|
|
||||||
bind:value={$FlightParametersStore.descent_rate}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormGroup 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 class="mb-2 d-grid gap-1">
|
|
||||||
<Button color="outline-secondary" size="sm">Показать график высоты</Button>
|
|
||||||
<Button color="secondary" size="sm">Сохранить как шаблон</Button>
|
|
||||||
<Button size="sm" color="primary" on:click={handleGetPrediction}>Выполнить прогнозирование</Button>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
{/if}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import { checkAuthenticated, logout } from '$lib/auth';
|
|
||||||
import {
|
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownToggle,
|
|
||||||
Nav,
|
|
||||||
NavItem,
|
|
||||||
NavLink,
|
|
||||||
Navbar,
|
|
||||||
NavbarBrand
|
|
||||||
} from '@sveltestrap/sveltestrap';
|
|
||||||
|
|
||||||
// Check if user is authenticated (using localStorage token)
|
|
||||||
let isAuthenticated = false;
|
|
||||||
|
|
||||||
// This should be reactive to changes in auth status
|
|
||||||
$: if (typeof window !== 'undefined') {
|
|
||||||
Promise.resolve(checkAuthenticated()).then(result => {
|
|
||||||
isAuthenticated = result;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
isAuthenticated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLogout() {
|
|
||||||
// Clear authentication tokens
|
|
||||||
try {
|
|
||||||
logout();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout failed:', error);
|
|
||||||
}
|
|
||||||
// Update auth status
|
|
||||||
isAuthenticated = false;
|
|
||||||
|
|
||||||
// Redirect to login page
|
|
||||||
goto('/');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Navbar color="light" light expand="lg" fixed="top" class="custom-navbar border-bottom">
|
|
||||||
<Nav class="me-auto mb-2 mb-lg-0" navbar>
|
|
||||||
<NavbarBrand href="/" class="nav-full-height">
|
|
||||||
<img src="/logo.svg" alt="Logo" height="34" class="d-inline-block align-text-top" />
|
|
||||||
</NavbarBrand>
|
|
||||||
<NavItem>
|
|
||||||
<NavLink
|
|
||||||
href="/predict"
|
|
||||||
class="nav-full-height border-bottom"
|
|
||||||
active={$page.url.pathname === '/predict'}
|
|
||||||
>
|
|
||||||
Прогнозирование
|
|
||||||
</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
<NavItem>
|
|
||||||
<NavLink
|
|
||||||
href="/track"
|
|
||||||
class="nav-full-height border-bottom"
|
|
||||||
active={$page.url.pathname === '/track'}
|
|
||||||
>
|
|
||||||
Слежение
|
|
||||||
</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
</Nav>
|
|
||||||
<Nav navbar>
|
|
||||||
{#if isAuthenticated}
|
|
||||||
<Dropdown nav inNavbar>
|
|
||||||
<DropdownToggle nav caret class="nav-full-height border-bottom">
|
|
||||||
Account
|
|
||||||
</DropdownToggle>
|
|
||||||
<DropdownMenu end>
|
|
||||||
<DropdownItem href="/user/account">
|
|
||||||
Account Settings
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem href="/user/templates">
|
|
||||||
Saved Templates
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem href="/user/predictions">
|
|
||||||
Prediction History
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem href="/user/flights">
|
|
||||||
Flight History
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem divider />
|
|
||||||
<DropdownItem on:click={handleLogout}>
|
|
||||||
Logout
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
{:else}
|
|
||||||
<NavItem>
|
|
||||||
<NavLink
|
|
||||||
href="/login"
|
|
||||||
class="nav-full-height border-bottom"
|
|
||||||
active={$page.url.pathname === '/login'}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
{/if}
|
|
||||||
</Nav>
|
|
||||||
</Navbar>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
//import { telemetryStore } from '../lib/telemetry.ts'; // Import your telemetry store
|
|
||||||
|
|
||||||
let telemetry = {};
|
|
||||||
let isCollapsed = false;
|
|
||||||
|
|
||||||
// Subscribe to the telemetry store
|
|
||||||
//const unsubscribe = telemetryStore.subscribe((data) => {
|
|
||||||
// telemetry = data;
|
|
||||||
//});
|
|
||||||
|
|
||||||
telemetry = {
|
|
||||||
latitude: 56.3576,
|
|
||||||
longitude: 39.8666,
|
|
||||||
altitude: 1000
|
|
||||||
};
|
|
||||||
|
|
||||||
// onMount(() => {
|
|
||||||
// return () => {
|
|
||||||
// unsubscribe(); // Cleanup subscription on component destroy
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="card shadow-lg position-absolute bottom-0 end-0 m-3" style="width: 23rem; max-height: 80vh; overflow-y: auto; z-index: 1000;">
|
|
||||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="card-title mb-0">Последние данные телеметрии</h6>
|
|
||||||
<button class="btn btn-sm btn-primary" on:click={() => (isCollapsed = !isCollapsed)}>
|
|
||||||
{#if isCollapsed}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-left-fill" viewBox="0 0 16 16">
|
|
||||||
<path d="m3.86 8.753 5.482 4.796c.646.566 1.658.106 1.658-.753V3.204a1 1 0 0 0-1.659-.753l-5.48 4.796a1 1 0 0 0 0 1.506z"/>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-down" viewBox="0 0 16 16">
|
|
||||||
<path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{#if !isCollapsed}
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label small">Широта:</label>
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<input type="text" class="form-control" value={telemetry.latitude || 'N/A'} readonly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label small">Долгота:</label>
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<input type="text" class="form-control" value={telemetry.longitude || 'N/A'} readonly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label small">Высота (м):</label>
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<input type="text" class="form-control" value={telemetry.altitude || 'N/A'} readonly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.card {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.card-header {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Map from "../Map.svelte";
|
import Map from "$lib/components/Map.svelte";
|
||||||
import ControlPanel from "../ControlPanel.svelte";
|
import ControlPanel from "$lib/components/ControlPanel.svelte";
|
||||||
import Navbar from "../Navbar.svelte";
|
import Navbar from "$lib/components/Navbar.svelte";
|
||||||
|
import PanelContainer from "$lib/components/PanelContainer.svelte";
|
||||||
|
import TelemetryPanel from '$lib/components/TelemetryPanel.svelte';
|
||||||
|
import ScenarioPanel from "$lib/components/ScenarioPanel.svelte";
|
||||||
|
import TabComponent from "$lib/components/TabComponent.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { PredictionStore } from "$lib/stores";
|
import { PredictionStore } from "$lib/stores";
|
||||||
import { Modal } from "@sveltestrap/sveltestrap";
|
import { Modal, Icon } from "@sveltestrap/sveltestrap";
|
||||||
import { addToast, removeToast } from "../Toast.svelte"
|
import Toast, { addToast, removeToast } from "$lib/components/Toast.svelte";
|
||||||
import ToastContainer from '../Toast.svelte';
|
import ToastContainer from '$lib/components/Toast.svelte';
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
|
|
||||||
let map: Map | null = null;
|
let map: Map | null = null;
|
||||||
let panel: ControlPanel | null = null;
|
let panelContainer: PanelContainer | null = null;
|
||||||
|
let controlPanel: ControlPanel | null = null;
|
||||||
let selectionToastId: string | null = null;
|
let selectionToastId: string | null = null;
|
||||||
|
let activeTab: 'control' | 'scenario' | 'settings' | 'about' = 'scenario';
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
PredictionStore.subscribe((data) => {
|
PredictionStore.subscribe((data) => {
|
||||||
|
|
@ -20,10 +26,11 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log("ControlPanel mounted");
|
console.log("ControlPanel mounted");
|
||||||
console.log(panel);
|
console.log(panelContainer);
|
||||||
|
|
||||||
if (panel) {
|
if (panelContainer) {
|
||||||
let element = panel.getElement();
|
let element = panelContainer.getElement();
|
||||||
|
if (!element) return;
|
||||||
L.DomEvent.disableClickPropagation(element);
|
L.DomEvent.disableClickPropagation(element);
|
||||||
L.DomEvent.disableScrollPropagation(element);
|
L.DomEvent.disableScrollPropagation(element);
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +58,7 @@
|
||||||
|
|
||||||
function handleCoordinateSelection(event: CustomEvent<{ lat: number; lng: number }>) {
|
function handleCoordinateSelection(event: CustomEvent<{ lat: number; lng: number }>) {
|
||||||
const { lat, lng } = event.detail;
|
const { lat, lng } = event.detail;
|
||||||
panel?.updateLaunchPosition(lat, lng);
|
controlPanel?.updateLaunchPosition(lat, lng);
|
||||||
console.log(`Selected coordinates: ${lat}, ${lng}`);
|
console.log(`Selected coordinates: ${lat}, ${lng}`);
|
||||||
if (selectionToastId) {
|
if (selectionToastId) {
|
||||||
removeToast(selectionToastId);
|
removeToast(selectionToastId);
|
||||||
|
|
@ -63,7 +70,29 @@
|
||||||
<main>
|
<main>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<Map bind:this={map} mode="prediction" bind:data={$PredictionStore} on:coordinatesSelected={handleCoordinateSelection}>
|
<Map bind:this={map} mode="prediction" bind:data={$PredictionStore} on:coordinatesSelected={handleCoordinateSelection}>
|
||||||
<ControlPanel bind:this={panel} {handleClickSelectOnMap} />
|
<PanelContainer bind:this={panelContainer} >
|
||||||
|
<TabComponent
|
||||||
|
tabs={[
|
||||||
|
{ id: 'scenario', icon: 'activity', label: 'Сценарий' },
|
||||||
|
{ id: 'control', icon: 'sliders', label: 'Условия' },
|
||||||
|
{ id: 'settings', icon: 'gear', label: 'Настройки' },
|
||||||
|
{ id: 'about', icon: 'info-circle', label: 'О проекте' }
|
||||||
|
]}
|
||||||
|
bind:activeTab
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if activeTab === 'control'}
|
||||||
|
<ControlPanel {handleClickSelectOnMap} bind:this={controlPanel} />
|
||||||
|
{:else if activeTab === 'scenario'}
|
||||||
|
<ScenarioPanel />
|
||||||
|
{:else if activeTab === 'settings'}
|
||||||
|
<!-- <SettingsPanel /> -->
|
||||||
|
{:else if activeTab === 'about'}
|
||||||
|
<!-- <AboutPanel /> -->
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</PanelContainer>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</Map>
|
</Map>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Map from '../Map.svelte';
|
import Map from '$lib/components/Map.svelte';
|
||||||
import TelemetryPanel from '../TelemetryPanel.svelte';
|
import TelemetryPanel from '$lib/components/TelemetryPanel.svelte';
|
||||||
import Navbar from '../Navbar.svelte';
|
import Navbar from '$lib/components/Navbar.svelte';
|
||||||
// import BurstCalculator from './BurstCalculator.svelte';
|
// import BurstCalculator from './BurstCalculator.svelte';
|
||||||
|
|
||||||
let coordinates = {
|
let coordinates = {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Navbar from '../../Navbar.svelte';
|
import Navbar from '$lib/components/Navbar.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,26 @@
|
||||||
height: var(--navbar-height);
|
height: var(--navbar-height);
|
||||||
padding-top: 0rem;
|
padding-top: 0rem;
|
||||||
padding-bottom: 0rem;
|
padding-bottom: 0rem;
|
||||||
z-index: 1000;
|
z-index: 1002;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-full-height.nav-link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
padding-left: 1rem !important;
|
padding-left: 1rem !important;
|
||||||
padding-right: 1rem !important;
|
padding-right: 1rem !important;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
background-color: var(--bs-light);
|
background-color: white;
|
||||||
margin-right: 1px;
|
margin-right: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:hover {
|
.nav-full-height.nav-link:hover {
|
||||||
color: white !important;;
|
color: white !important;;
|
||||||
background-color: var(--bs-primary);
|
background-color: var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link.active {
|
.nav-full-height.nav-link.active {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
background-color: var(--bs-primary);
|
background-color: var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
.navbar {
|
.navbar {
|
||||||
z-index: 1001;
|
z-index: 1002;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|
@ -51,6 +51,8 @@
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--navbar-height: 44px;
|
--navbar-height: 44px;
|
||||||
|
--panel-left: 20px;
|
||||||
|
--panel-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-container {
|
.map-container {
|
||||||
|
|
@ -69,7 +71,6 @@
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
width: auto;
|
width: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
@ -77,7 +78,45 @@
|
||||||
|
|
||||||
.panel-container {
|
.panel-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
top: var(--panel-top);
|
||||||
left: 20px;
|
left: var(--panel-left);
|
||||||
z-index: 1000;
|
width: 23rem;
|
||||||
}
|
max-height: 90vh;
|
||||||
|
max-width: calc(100vw - var(--panel-left) - var(--panel-left));
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
|
||||||
|
border-radius: var(--bs-border-radius) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-tooltip-top::before {
|
||||||
|
border-top-color: var(--bs-border-color) !important;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom::before {
|
||||||
|
border-bottom-color: var(--bs-border-color) !important;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left::before {
|
||||||
|
border-left-color: var(--bs-border-color) !important;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right::before {
|
||||||
|
border-right-color: var(--bs-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-tooltip {
|
||||||
|
background-color: var(--bs-body-bg) !important;
|
||||||
|
border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
|
||||||
|
border-radius: var(--bs-border-radius) !important;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 767.98px)
|
||||||
|
{
|
||||||
|
.coordinates-display {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
static/ext/leaflet-ruler/icon.png
Normal file
BIN
static/ext/leaflet-ruler/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 756 B |
41
static/ext/leaflet-ruler/leaflet-ruler.css
Normal file
41
static/ext/leaflet-ruler/leaflet-ruler.css
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
.leaflet-ruler{
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
background-image: url("./icon.png"); /* <div>Icons made by <a href="http://www.freepik.com" title="Freepik">Freepik</a> from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div> */
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
.leaflet-ruler:hover{
|
||||||
|
background-image: url("./icon.png"); /* <div>Icons made by <a href="http://www.freepik.com" title="Freepik">Freepik</a> from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div> */
|
||||||
|
}
|
||||||
|
.leaflet-ruler-clicked{
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url("./icon.png");
|
||||||
|
border-color: chartreuse !important;
|
||||||
|
}
|
||||||
|
.leaflet-bar{
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.result-tooltip{
|
||||||
|
background-color: white;
|
||||||
|
border-width: medium;
|
||||||
|
border-color: #de0000;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
.moving-tooltip{
|
||||||
|
background-color: rgba(255, 255, 255, .7);
|
||||||
|
background-clip: padding-box;
|
||||||
|
opacity: 0.5;
|
||||||
|
border: dotted;
|
||||||
|
border-color: red;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
.plus-length{
|
||||||
|
padding-left: 45px;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue