Compare commits

...
Sign in to create a new pull request.

26 commits

Author SHA1 Message Date
Vasilisk9812
e428f55580 Pre timeControl 2025-07-02 20:44:12 +09:00
Vasilisk9812
aa0ff91a7d heatmap 2025-06-30 02:50:17 +09:00
Vasilisk9812
74340cf28e wind view checkbox 2025-06-27 22:15:02 +09:00
Vasilisk9812
f4b397043a pre heatmap 2025-06-27 21:44:08 +09:00
ThePetrovich
72c0d5e609 Fix navbar styles & map position 2025-06-27 20:07:54 +08:00
ThePetrovich
eb29cdc585 Prevent propagation on panel 2025-06-27 19:58:50 +08:00
ThePetrovich
52558ed3b2 Continue messing with stores 2025-06-27 19:27:19 +08:00
ThePetrovich
c7df38e6ce Refactor of map & other components 2025-06-27 18:23:50 +08:00
ThePetrovich
527d4417ff fix login & add sveltestrap 2025-06-26 19:15:33 +08:00
Vasilisk9812
a822fb1e36 wind-global 2025-06-21 18:18:15 +09:00
Vasilisk9812
79848ef36f login 2025-04-06 01:14:40 +09:00
ThePetrovich
19a8cdc1d6 Merge branch 'components' of https://git.intra.yksa.space/mikhailov.aa/leaflet_svelte into components 2025-04-05 23:12:38 +08:00
ThePetrovich
14132dfeb6 login page layout 2025-04-05 23:11:56 +08:00
Vasilisk9812
0b4f0fe6d8 authorization prework 2025-04-06 00:10:25 +09:00
Vasilisk9812
29d7480753 login navbar add 2025-04-05 23:54:58 +09:00
ThePetrovich
522202b89e Merge branch 'components' of https://git.intra.yksa.space/mikhailov.aa/leaflet_svelte into components 2025-04-05 22:45:58 +08:00
ThePetrovich
afc45cc9cc Add routes 2025-04-05 22:44:34 +08:00
Vasilisk9812
51dc62a68f login page placeholder 2025-04-05 23:37:38 +09:00
ThePetrovich
55295b84aa Add initial plotting 2025-04-05 14:43:23 +08:00
ThePetrovich
2db5d14202 Merge branch 'components' of https://git.intra.yksa.space/mikhailov.aa/leaflet_svelte into components 2025-04-05 01:23:40 +08:00
ThePetrovich
859966c48d add navbar 2025-04-05 01:23:03 +08:00
Vasilisk9812
6bd3a656f9 profile fix 2025-04-05 02:11:48 +09:00
ThePetrovich
cd98f04622 Merge branch 'components' of https://git.intra.yksa.space/mikhailov.aa/leaflet_svelte into components 2025-04-04 23:42:35 +08:00
ThePetrovich
68aae97597 add bootstrap 2025-04-04 23:42:30 +08:00
Vasilisk9812
e67a9c6455 request 2025-04-05 00:14:36 +09:00
Vasilisk9812
0f130c640c components 2025-04-04 22:57:35 +09:00
40 changed files with 132530 additions and 800 deletions

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"tabWidth": 4,
"endOfLine": "lf",
"printWidth": 120,
"useTabs": false
}

View file

@ -3,6 +3,7 @@
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"allowImportingTsExtensions": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,

593
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{ {
"name": "project", "name": "app4",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",
"type": "module", "type": "module",
@ -15,13 +15,19 @@
"@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.16.0", "@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.0",
"svelte": "^5.0.0", "svelte": "^5.34.8",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^6.0.0" "vite": "^6.2.5"
}, },
"dependencies": { "dependencies": {
"@sveltestrap/sveltestrap": "^7.1.0",
"bootstrap-icons": "^1.11.3",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"svelte-map-leaflet": "^0.5.0" "leaflet-heatmap": "^1.0.0",
"leaflet-timedimension": "^1.1.1",
"leaflet-velocity": "^2.1.4",
"leaflet.heat": "^0.2.0"
} }
} }

View file

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -3,8 +3,15 @@
<head> <head>
<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/custom.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
<style>
body {
background-color: #f5f5f5;
}
</style>
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>

80
src/lib/auth.ts Normal file
View file

@ -0,0 +1,80 @@
import Cookies from 'js-cookie';
export const CSRF_URL = 'http://localhost:8000/api/csrf/';
export const LOGIN_URL = 'http://localhost:8000/api/login/';
export const LOGOUT_URL = 'http://localhost:8000/api/logout/';
export const SESSION_URL = 'http://localhost:8000/api/session/';
export const WHOAMI_URL = 'http://localhost:8000/api/whoami/';
export async function getCsrfToken(): Promise<string | null> {
return Cookies.get('csrftoken') || null;
}
export async function getCsrfTokenAuth(): Promise<string | null> {
const response = await fetch(CSRF_URL, {});
console.log('CSRF Token Response:', response);
return Cookies.get('csrftoken') || null;
}
export async function checkAuthenticated(): Promise<boolean> {
const csrfToken = await getCsrfTokenAuth();
if (!csrfToken) {
throw new Error('CSRF token not found');
}
const response = await fetch(SESSION_URL, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
});
let data = await (response as Response).json();
return data.isAuthenticated;
}
export async function login(username: string, password: string): Promise<void> {
const csrfToken = await getCsrfTokenAuth();
if (!csrfToken) {
throw new Error('CSRF token not found');
}
const response = await fetch(LOGIN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ username, password }),
credentials: 'include'
});
if (!response.ok) {
throw new Error(`Login failed: ${response.statusText}`);
}
const data = await response.json();
return data;
}
export async function logout(): Promise<void> {
const csrfToken = await getCsrfTokenAuth();
if (!csrfToken) {
throw new Error('CSRF token not found');
}
const response = await fetch(LOGOUT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include'
});
if (!response.ok) {
throw new Error(`Logout failed: ${response.statusText}`);
}
console.log('Logout successful');
return;
}

24
src/lib/mathutil.ts Normal file
View file

@ -0,0 +1,24 @@
export function distHaversine(
p1: { lat: number; lng: number },
p2: { lat: number; lng: number },
precision?: number
): string {
const R = 6371; // Earth's mean radius in km
const rad = (x: number): number => (x * Math.PI) / 180;
const dLat = rad(p2.lat - p1.lat);
const dLong = rad(p2.lng - p1.lng);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(rad(p1.lat)) *
Math.cos(rad(p2.lat)) *
Math.sin(dLong / 2) *
Math.sin(dLong / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const d = R * c;
return d.toFixed(precision ?? 3);
}

157
src/lib/prediction.ts Normal file
View file

@ -0,0 +1,157 @@
import { writable } from "svelte/store";
import type { LatLngExpression } from "leaflet";
import L from "leaflet";
import { getCsrfToken } from "./auth";
import type { PredictionStage, RawPrediction, Prediction } from "./types";
import { PredictionStore, RawPredictionStore, writeLocalStorage } from "./stores";
function getLatestDataset() {
const now = new Date();
const hours = now.getUTCHours();
const minutes = now.getUTCMinutes();
const seconds = now.getUTCSeconds();
// Round down to the nearest 6-hour interval
const roundedHours = Math.floor(hours / 6) * 6;
const roundedDate = new Date(now);
roundedDate.setUTCHours(roundedHours, 0, 0, 0);
// Subtract 6 hours to account for the lag
roundedDate.setUTCHours(roundedDate.getUTCHours() - 6);
return roundedDate.toISOString();
}
function formatLaunchDateTime(dateObj: string | Date, timeStr: string): string {
// Ensure date is a Date object
const date = new Date(dateObj);
// Extract date components
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
// Format time (ensure it has seconds)
let formattedTime = timeStr;
if (timeStr.split(":").length === 2) {
formattedTime += ":00"; // Add seconds if missing
}
// Combine into ISO string
const isoString = new Date(`${year}-${month}-${day}T${formattedTime}Z`).toISOString();
return isoString;
}
export const getForecast = async (
flightParameters: Record<string, any>,
): Promise<void> => {
// Create request object
flightParameters.dataset = getLatestDataset();
console.log("Sending request:", flightParameters);
try {
// Example POST request - replace with your actual API endpoint
const csrfToken = await getCsrfToken();
if (!csrfToken) {
throw new Error("CSRF token not found");
}
const response = await fetch("http://localhost:8000/api/predictions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken,
},
body: JSON.stringify(flightParameters),
credentials: "include",
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("Forecast response:", data);
RawPredictionStore.set(data.result as RawPrediction);
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);
}
};
export function parsePrediction(prediction: PredictionStage[]): Prediction {
const flight_path: [number, number, number][] = [];
const launch: { latlng: LatLngExpression; datetime: Date } = {} as any;
const burst: { latlng: LatLngExpression; datetime: Date } = {} as any;
const landing: { latlng: LatLngExpression; datetime: Date } = {} as any;
const ascent = prediction[0].trajectory;
const descent = prediction[1].trajectory;
// Add the ascent track to the flight path array.
ascent.forEach((item) => {
let lon = item.longitude;
if (lon > 180.0) {
lon -= 360.0;
}
flight_path.push([item.latitude, lon, item.altitude]);
});
// Add the descent track to the flight path array.
descent.forEach((item) => {
let lon = item.longitude;
if (lon > 180.0) {
lon -= 360.0;
}
flight_path.push([item.latitude, lon, item.altitude]);
});
// Populate the launch, burst, and landing points
const launchObj = ascent[0];
let lon = launchObj.longitude;
if (lon > 180.0) {
lon -= 360.0;
}
launch.latlng = L.latLng([launchObj.latitude, lon, launchObj.altitude]);
launch.datetime = new Date(launchObj.datetime);
const burstObj = descent[0];
lon = burstObj.longitude;
if (lon > 180.0) {
lon -= 360.0;
}
burst.latlng = L.latLng([burstObj.latitude, lon, burstObj.altitude]);
burst.datetime = new Date(burstObj.datetime);
const landingObj = descent[descent.length - 1];
lon = landingObj.longitude;
if (lon > 180.0) {
lon -= 360.0;
}
landing.latlng = L.latLng([landingObj.latitude, lon, landingObj.altitude]);
landing.datetime = new Date(landingObj.datetime);
const profile = prediction[1].stage === "descent" ? "standard_profile" : "float_profile";
const flight_time = (new Date(landing.datetime).getTime() - new Date(launch.datetime).getTime()) / 1000;
return {
flight_path,
launch,
burst,
landing,
profile,
flight_time,
};
}

69
src/lib/stores.ts Normal file
View file

@ -0,0 +1,69 @@
import { writable } from "svelte/store";
import type { FlightParameters, RawTelemetry, Telemetry } from "./types";
import type { RawPrediction, Prediction } from "./types";
export const readLocalStorage = <T>(key: string, defaultValue: T): T => {
const item = localStorage.getItem(key);
if (item) {
try {
const parsed = JSON.parse(item);
if (typeof parsed === "object" && parsed !== null) {
return parsed as T;
}
} catch (error) {
console.error(`Error parsing ${key} from localStorage:`, error);
}
}
return defaultValue;
};
export const writeLocalStorage = <T>(key: string, value: T): void => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error writing ${key} to localStorage:`, error);
}
}
export const clearLocalStorage = (key: string): void => {
try {
localStorage.removeItem(key);
}
catch (error) {
console.error(`Error clearing ${key} from localStorage:`, error);
}
}
const flightParametersDefaults: FlightParameters = {
ascent_rate: 5.0,
burst_altitude: 30000.0,
dataset: "",
descent_rate: 5.0,
format: "json",
launch_altitude: 0.0,
launch_datetime: "",
launch_latitude: 62.1234,
launch_longitude: 129.1234,
profile: "standard_profile",
version: 2,
};
export const FlightParametersStore = writable<FlightParameters>(
readLocalStorage<FlightParameters>("flightParameters", flightParametersDefaults)
);
export const RawTelemetryStore = writable<RawTelemetry>(
readLocalStorage<RawTelemetry>("rawTelemetry", {} as RawTelemetry)
);
export const TelemetryStore = writable<Telemetry>(
readLocalStorage<Telemetry>("telemetry", {} as Telemetry)
);
export const RawPredictionStore = writable<RawPrediction>(
readLocalStorage<RawPrediction>("rawPrediction", {} as RawPrediction)
);
export const PredictionStore = writable<Prediction>(
readLocalStorage<Prediction>("prediction", {} as Prediction)
);

23
src/lib/telemetry.ts Normal file
View file

@ -0,0 +1,23 @@
import { writable } from "svelte/store"
import L from "leaflet";
import type { TelemetryPoint, Telemetry } from "./types";
export function parseTelemetry(telemetry: TelemetryPoint[]): Telemetry {
const flight_path: [number, number, number][] = telemetry.map((point) => [
point.latitude,
point.longitude,
point.altitude
]);
const launch = {
latlng: L.latLng(telemetry[0].latitude, telemetry[0].longitude),
datetime: new Date(telemetry[0].datetime)
};
return {
flight_path,
launch,
datapoints: telemetry
};
}

111
src/lib/types.ts Normal file
View file

@ -0,0 +1,111 @@
import type { LatLngExpression, LatLngLiteral } from "leaflet";
export const PROFILE_MAP = {
Normal: "standard_profile",
Float: "float_profile",
Reverse: "reverse_profile",
Custom: "custom_profile",
};
export type ProfileName = keyof typeof PROFILE_MAP;
export interface FlightParameters {
ascent_rate: number;
burst_altitude: number;
dataset: string;
descent_rate: number;
format: "json";
launch_altitude: number;
launch_datetime: string;
launch_latitude: number;
launch_longitude: number;
profile: (typeof PROFILE_MAP)[ProfileName];
version: number;
}
export interface TelemetryPoint {
altitude: number;
datetime: string;
latitude: number;
longitude: number;
payload: string;
}
export interface TelemetryMetadata {
complete_datetime: string;
start_datetime: string;
}
export interface RawTelemetry {
metadata: TelemetryMetadata;
telemetry: TelemetryPoint[];
}
export interface Telemetry {
flight_path: [number, number, number][];
launch: {
latlng: LatLngExpression;
datetime: Date;
};
datapoints: TelemetryPoint[];
}
export interface TrajectoryPoint {
altitude: number;
datetime: string;
latitude: number;
longitude: number;
}
export interface PredictionStage {
stage: string;
trajectory: TrajectoryPoint[];
}
export interface PredictionMetadata {
complete_datetime: string;
start_datetime: string;
}
export interface RawPrediction {
metadata: PredictionMetadata;
prediction: PredictionStage[];
}
export interface Prediction {
flight_path: [number, number, number][];
launch: {
latlng: LatLngExpression;
datetime: Date;
};
burst: {
latlng: LatLngExpression;
datetime: Date;
};
landing: {
latlng: LatLngExpression;
datetime: Date;
};
profile: string;
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[];
}

View file

@ -1,7 +1,7 @@
<script> <script>
import Map from './leaflet.svelte'; import Navbar from './Navbar.svelte';
</script> </script>
<main> <main>
<Map /> <Navbar />
</main> </main>

View file

@ -0,0 +1,288 @@
<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";
export 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.
}
};
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.toString();
inputLng = lng.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 bottom-0 end-0 m-3">
<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={() => console.log("Select on map clicked")}>Указать на карте</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>

109
src/routes/Navbar.svelte Normal file
View file

@ -0,0 +1,109 @@
<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>

View file

@ -0,0 +1,74 @@
<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>

View file

View file

@ -0,0 +1,336 @@
<script>
import { onMount, onDestroy } from 'svelte';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet-velocity/dist/leaflet-velocity.css';
import 'leaflet-velocity/dist/leaflet-velocity';
import 'leaflet.heat';
import 'leaflet-timedimension';
export let map; // принимаем карту из родительского компонента
export let windData;
let timeDimension;
let timeDimensionControl;
let velocityLayer;
let heatLayer;
let legend;
// Состояние переключателей
let showHeatmap = true;
let showVectors = true;
let layerControl;
// Преобразование testVelo.json в формат timeData
const prepareTimeData = (windData) => {
if (!windData || windData.length < 2) return {};
// Используем дату из header или текущую дату, если не указана
const refTime = windData[0]?.header?.refTime || new Date().toISOString();
return {
[refTime]: {
u: windData[0].data, // U-компонента (первый объект в массиве)
v: windData[1].data // V-компонента (второй объект)
}
};
};
// Функция для нормализации данных тепловой карты
const prepareHeatData = (windData) => {
if (!windData || windData.length < 2) {
console.warn("Invalid wind data structure");
return [];
}
// Получаем U и V компоненты
const uComponent = windData.find(item => item.header.parameterNumber === 2);
const vComponent = windData.find(item => item.header.parameterNumber === 3);
if (!uComponent || !vComponent) {
console.warn("Missing wind components");
return [];
}
const header = uComponent.header; // Используем header из U компоненты
const { lo1, la1, dx, dy, nx, ny } = header;
const heatData = [];
let maxSpeed = 0;
// Проверяем совпадение размеров данных
if (uComponent.data.length !== vComponent.data.length) {
console.warn("U and V components have different lengths");
return [];
}
// Собираем данные и находим максимальную скорость
for (let i = 0; i < uComponent.data.length; i++) {
const u = uComponent.data[i];
const v = vComponent.data[i];
const speed = Math.sqrt(u * u + v * v);
if (!isNaN(speed)) {
// Вычисляем координаты для текущей точки
const y = Math.floor(i / nx);
const x = i % nx;
let lat = la1 - y * dy;
let lng = lo1 + x * dx;
if (lng >= 180) lng -= 360;
heatData.push([lat, lng, speed]);
maxSpeed = Math.max(maxSpeed, speed);
}
}
console.log(`Prepared heat data: ${heatData.length} points, max speed: ${maxSpeed}`);
// Нормализуем значения интенсивности от 0 до 1
if (maxSpeed > 0) {
return heatData.map(([lat, lng, intensity]) => [lat, lng, intensity / maxSpeed]);
}
return heatData;
};
// Создание тепловой карты
const createHeatLayer = (data) => {
if (!data || data.length === 0) {
console.warn("No valid heat data provided");
return null;
}
try {
return L.heatLayer(data, {
radius: 8, // Увеличьте радиус для глобальной карты
blur: 20,
// maxZoom: 10,
minOpacity: 0.7,
gradient: {
0.1: 'blue',
0.3: 'cyan',
0.5: 'lime',
0.7: 'yellow',
1.0: 'red'
}
});
} catch (e) {
console.error("Failed to create heat layer:", e);
return null;
}
};
// Обновление слоев
const updateLayers = () => {
if (!map || !windData) return;
// Удаляем старые слои
if (velocityLayer) map.removeLayer(velocityLayer);
if (heatLayer) map.removeLayer(heatLayer);
if (legend) map.removeControl(legend);
// Создаем слой векторов ветра
if (showVectors) {
velocityLayer = L.velocityLayer({
displayValues: true,
displayOptions: {
velocityType: 'Wind Speed',
position: 'bottomleft',
emptyString: 'No wind data',
},
data: windData
}).addTo(map);
}
// Создаем тепловую карту
if (showHeatmap) {
const heatData = prepareHeatData(windData);
heatLayer = createHeatLayer(heatData);
if (heatLayer) {
heatLayer.addTo(map);
createLegend(Math.max(...heatData.map(point => point[2])));
}
}
// Обновляем контроль слоев
updateLayerControl();
};
const updateLayerControl = () => {
if (layerControl) {
map.removeControl(layerControl);
}
const overlays = {};
if (velocityLayer) {
overlays['Векторы ветра'] = velocityLayer;
}
if (heatLayer) {
overlays['Тепловая карта'] = heatLayer;
}
layerControl = L.control.layers(null, overlays, {
collapsed: false,
position: 'topright'
}).addTo(map);
};
// Создание легенды с учетом максимальной скорости
const createLegend = (maxSpeed) => {
if (!map) return;
legend = L.control({ position: 'bottomright' });
legend.onAdd = () => {
const div = L.DomUtil.create('div', 'wind-heat-legend');
div.innerHTML = `
<h4>Wind Speed (m/s)</h4>
<div class="legend-scale">
<div class="legend-color" style="background: #0000FF;"></div>
<div class="legend-color" style="background: #00FFFF;"></div>
<div class="legend-color" style="background: #00FF00;"></div>
<div class="legend-color" style="background: #FFFF00;"></div>
<div class="legend-color" style="background: #FF0000;"></div>
</div>
<div class="legend-labels">
<span>0</span>
<span>${(maxSpeed * 0.25).toFixed(1)}</span>
<span>${(maxSpeed * 0.5).toFixed(1)}</span>
<span>${(maxSpeed * 0.75).toFixed(1)}</span>
<span>${maxSpeed.toFixed(1)}</span>
</div>
`;
return div;
};
legend.addTo(map);
};
onMount(() => {
if (!map) return;
// 1. Настройка TimeDimension (добавьте эти строки в начале)
// L.TimeDimension.Util.setProxy('https://your-proxy.com/?url='); // Для загрузки больших данных
L.TimeDimension.Util.setCacheLimit(10); // Лимит кэшированных кадров
// 1. Подготовка данных
const timeData = prepareTimeData(windData);
const firstTime = Object.keys(timeData)[0];
// Инициализация TimeDimension
timeDimension = new L.TimeDimension({
period: "PT1H", // Интервал 1 час
timeInterval: '${firstTime}/${firstTime}',
});
// Добавляем контролы времени
timeDimensionControl = new L.Control.TimeDimension({
timeDimension,
position: 'bottomleft',
// autoPlay: true,
playerOptions: {
// transitionTime: 1000,
loop: false,
minBufferReady: -1
}
});
map.addControl(timeDimensionControl);
// 4. Создание слоев
const velocityLayer = L.timeDimension.layer.windVelocity({
displayValues: true,
data: timeData,
displayOptions: {
velocityType: 'Wind Speed',
position: 'bottomleft'
}
}).addTo(map);
// 5. Тепловая карта (адаптируйте под ваш формат)
const heatLayer = L.timeDimension.layer.heat({
radius: 15,
data: prepareTimeHeatData(timeData)
}).addTo(map);
});
onDestroy(() => {
if (map) {
if (velocityLayer) map.removeLayer(velocityLayer);
if (heatLayer) map.removeLayer(heatLayer);
if (legend) map.removeControl(legend);
}
});
// Реактивность на изменение параметров
$: if (map && windData) {
updateLayers();
};
</script>
<div class="layer-controls">
<div class="control-group">
<label>
<input type="checkbox" bind:checked={showHeatmap}> Тепловая карта
</label>
<label>
<input type="checkbox" bind:checked={showVectors}> Векторы ветра
</label>
</div>
</div>
<style>
.layer-controls {
position: absolute;
bottom: 30px;
left: 10px;
z-index: 1000;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.control-group label {
display: flex;
align-items: center;
gap: 5px;
font-size: 14px;
cursor: pointer;
}
:global(.wind-heat-legend) {
padding: 8px 10px;
background: rgba(255, 255, 255, 0.9);
border-radius: 5px;
box-shadow: 0 0 15px rgba(0,0,0,0.2);
line-height: 1.2;
color: #333;
font-family: Arial, sans-serif;
}
:global(.wind-heat-legend h4) {
margin: 0 0 5px;
font-size: 14px;
font-weight: bold;
}
:global(legend-scale) {
display: flex;
margin-bottom: 3px;
}
:global(legend-color) {
height: 12px;
flex-grow: 1;
}
:global(.legend-labels) {
display: flex;
justify-content: space-between;
font-size: 11px;
}
</style>

View file

@ -1,548 +0,0 @@
<script>
import { onMount } from 'svelte';
import * as L from 'leaflet';
import 'leaflet/dist/leaflet.css';
/**
* @type {{ removeLayer: (arg0: any) => void; setView: (arg0: number[], arg1: any) => void; getZoom: () => any; on: (arg0: string, arg1: (e: any) => void) => void; }}
*/
let map;
let mouseLat = 0;
let mouseLng = 0;
/**
* @type {null}
*/
let marker = null;
let startPoint = 'Custom';
let startHeight = 0;
let startTime = '13:13';
let startDate = new Date(2025, 2, 24);
let ascentRate = 5;
let burstAltitude = 30000;
let flightProfile = 'Normal';
let descentRate = 5;
let forecastMode = 'Single';
let inputLat = '56.3576';
let inputLng = '39.8666';
let showBurstCalculator = false;
let payloadMass = 1500;
let balloonMass = 1000;
let desiredBurstAltitude = 33000;
let desiredAscentRate = 2.33;
let burstAltitudeResult = 33000;
let timeToBurst = 236;
let initialVolume = 2.66;
let ascentRateResult = 2.33;
let liftForce = 1733;
let volumeLiters = 2662;
let volumeCubicFeet = 94.0;
const toggleBurstCalculator = () => {
showBurstCalculator = !showBurstCalculator;
};
const calculateBurst = () => {
// In a real app, you would implement actual calculations here
// These are just placeholder values matching your image
burstAltitudeResult = desiredBurstAltitude;
timeToBurst = 236;
initialVolume = 2.66;
ascentRateResult = desiredAscentRate;
liftForce = 1733;
volumeLiters = 2662;
volumeCubicFeet = 94.0;
};
const updateMapPosition = () => {
const lat = parseFloat(inputLat);
const lng = parseFloat(inputLng);
if (isNaN(lat)) {
alert("Please enter a valid latitude");
return;
}
if (isNaN(lng)) {
alert("Please enter a valid longitude");
return;
}
if (lat < -90 || lat > 90) {
alert("Latitude must be between -90 and 90");
return;
}
if (lng < -180 || lng > 180) {
alert("Longitude must be between -180 and 180");
return;
}
// Remove existing marker if it exists
if (marker) {
map.removeLayer(marker);
}
// Create new marker
marker = L.marker([lat, lng]).addTo(map)
.bindPopup("Launch Point");
// Center map on new coordinates
map.setView([lat, lng], map.getZoom());
};
onMount(() => {
map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
map.on('mousemove', (e) => {
mouseLat = e.latlng.lat.toFixed(6);
mouseLng = e.latlng.lng.toFixed(6);
});
marker = L.marker([56.3576, 39.8666]).addTo(map)
.bindPopup(() => {
return `
<b>Launch Point</b><br>, Lat: ${marker.getLatLng().lat.toFixed(6)}<br>, Long: ${marker.getLatLng().lng.toFixed(6)}<br>
`;
});
});
// Forecast request function
const getForecast = async () => {
// Create request object
const request = {
ascent_rate: parseFloat(ascentRate),
burst_altitude: parseFloat(burstAltitude),
dataset: new Date().toISOString(), // Current time as dataset timestamp
descent_rate: parseFloat(descentRate),
format: "json",
launch_altitude: parseFloat(startHeight),
launch_datetime: new Date(
`${startDate.getFullYear()}-${startDate.getMonth() + 1}-${startDate.getDate()}T${startTime}:00Z`
).toISOString(),
launch_latitude: parseFloat(inputLat),
launch_longitude: parseFloat(inputLng),
profile: flightProfile === 'Normal' ? 'standard_profile' : 'custom_profile',
version: 2
};
console.log("Sending request:", request);
try {
// Example POST request - replace with your actual API endpoint
const response = await fetch('https://api.example.com/forecast', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("Forecast response:", data);
alert("Forecast request successful!");
// Handle the response data as needed
} catch (error) {
console.error("Error sending forecast request:", error);
alert("Error getting forecast: " + error.message);
}
};
// Helper function to format date as YYYY-MM-DD
const formatDateForAPI = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
</script>
<div class="map-container">
<div id="map"></div>
<div class="coordinates-display">
Lat: {mouseLat}, Long: {mouseLng}
</div>
<div class="control-panel">
<div class="control-header">Launch Point</div>
<div class="control-row">
<span>Start Point:</span>
<select bind:value={startPoint}>
<option>Custom</option>
<option>Preset 1</option>
<option>Preset 2</option>
</select>
</div>
<div class="control-row">
<span>Latitude/Longitude:</span>
<div class="coordinate-inputs">
<input type="text" bind:value={inputLat} placeholder="Latitude">
<p>/</p>
<input type="text" bind:value={inputLng} placeholder="Longitude">
<button on:click={updateMapPosition} class="update-button"></button>
</div>
</div>
<div class="control-row">
<button class="map-button" on:click={() => {
inputLat = mouseLat;
inputLng = mouseLng;
updateMapPosition();
}}>Specify on map (click location)</button>
</div>
<div class="control-row">
<span>Launch Height (m):</span>
<input type="number" bind:value={startHeight}>
</div>
<div class="control-row">
<span>Launch Time (UTC):</span>
<input type="time" bind:value={startTime}>
</div>
<div class="control-row">
<span>Launch Date:</span>
<input type="date" bind:value={startDate}>
</div>
<div class="control-row">
<span>Ascent Rate (m/s):</span>
<input type="number" bind:value={ascentRate}>
</div>
<div class="control-row">
<span>Burst/Drift Altitude (m):</span>
<input type="number" bind:value={burstAltitude}>
</div>
<div class="control-row">
<span>Flight Profile:</span>
<select bind:value={flightProfile}>
<option>Normal</option>
<option>Drift</option>
<option>Reversible (on the rise)</option>
<option>Custom</option>
</select>
</div>
<div class="control-row buttons">
<button on:click={toggleBurstCalculator}>Open Burst Calculator</button>
<button>Set Custom Flight Profile</button>
<button>Show Last Altitude Graph</button>
</div>
<div class="control-row">
<span>Descent Rate (m/s):</span>
<input type="number" bind:value={descentRate}>
</div>
<div class="control-row">
<span>Forecast Mode (help):</span>
<div class="radio-group">
<label>
<input type="radio" bind:group={forecastMode} value="Single"> Single
</label>
<label>
<input type="radio" bind:group={forecastMode} value="Multiple"> Multiple
</label>
</div>
</div>
<div class="control-row">
<button class="primary-button" on:click={getForecast}>Get Forecast</button>
<button>Save Point</button>
</div>
</div>
</div>
<!-- Burst Calculator Modal -->
{#if showBurstCalculator}
<div class="modal-overlay" on:click|self={toggleBurstCalculator}>
<div class="modal-content">
<h2>Balloon Burst Calculation</h2>
<div class="calculator-grid">
<div class="input-group">
<label>Payload Mass (g)</label>
<input type="number" bind:value={payloadMass}>
</div>
<div class="input-group">
<label>Balloon Mass (g)</label>
<input type="number" bind:value={balloonMass}>
</div>
<div class="input-group">
<label>Desired Burst Altitude (m)</label>
<input type="number" bind:value={desiredBurstAltitude}>
</div>
<div class="input-group">
<label>Desired Ascent Rate (m/s)</label>
<input type="number" bind:value={desiredAscentRate} step="0.01">
</div>
</div>
<div class="results-section">
<h3>Results</h3>
<div class="result-row">
<span>Burst Altitude:</span>
<span>{burstAltitudeResult} m</span>
</div>
<div class="result-row">
<span>Time to Burst:</span>
<span>{timeToBurst} min</span>
</div>
<div class="result-row">
<span>Initial Volume:</span>
<span>{initialVolume}</span>
</div>
<div class="result-row">
<span>Ascent Rate:</span>
<span>{ascentRateResult} m/s</span>
</div>
<div class="result-row">
<span>Lift Force at Launch:</span>
<span>{liftForce} g</span>
</div>
<div class="result-row">
<span>Volume:</span>
<span>{volumeLiters} L ({volumeCubicFeet} ft³)</span>
</div>
</div>
<div class="modal-actions">
<button class="secondary-button">Additional Settings</button>
<button class="primary-button" on:click={calculateBurst}>Use Results</button>
<button on:click={toggleBurstCalculator}>Close</button>
</div>
</div>
</div>
{/if}
<style>
.map-container {
position: relative;
width: 100%;
height: 100vh;
}
#map {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.coordinates-display {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.8);
padding: 5px 10px;
border-radius: 3px;
font-family: Arial, sans-serif;
font-size: 14px;
z-index: 1000; /* Ensure it's above the map */
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
.control-panel {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 5px;
font-family: Arial, sans-serif;
font-size: 14px;
z-index: 1000;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
width: 320px;
max-height: 80vh;
overflow-y: auto;
}
.control-header {
font-weight: bold;
margin-bottom: 10px;
font-size: 16px;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
}
.control-row {
margin-bottom: 8px;
display: flex;
align-items: center;
}
.control-row span {
width: 160px;
display: inline-block;
}
.control-row input[type="number"],
.control-row input[type="date"],
.control-row input[type="time"],
.control-row select {
width: 120px;
padding: 3px;
}
.coordinate-inputs {
display: flex;
align-items: center;
gap: 5px;
}
.coordinate-inputs input {
width: 70px !important;
padding: 3px;
}
.update-button {
padding: 3px 8px;
background: #4CAF50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
margin-left: 5px;
}
.map-button {
width: 100%;
padding: 5px;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
}
.buttons {
flex-direction: column;
gap: 5px;
margin-top: 10px;
}
.buttons button {
width: 100%;
padding: 5px;
margin-bottom: 5px;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
}
.primary-button {
background: #4CAF50 !important;
color: white;
border: 1px solid #3e8e41 !important;
}
.radio-group {
display: flex;
gap: 10px;
}
.radio-group label {
display: flex;
align-items: center;
gap: 5px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
width: 500px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.calculator-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin: 20px 0;
}
.input-group {
display: flex;
flex-direction: column;
}
.input-group label {
margin-bottom: 5px;
font-weight: bold;
font-size: 0.9em;
}
.input-group input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.results-section {
background-color: #f5f5f5;
padding: 15px;
border-radius: 5px;
margin: 15px 0;
}
.results-section h3 {
margin-top: 0;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
}
.result-row {
display: flex;
justify-content: space-between;
margin: 8px 0;
}
.result-row span:first-child {
font-weight: bold;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.secondary-button {
background: #f0f0f0;
border: 1px solid #ccc;
}
</style>

View file

@ -0,0 +1,97 @@
<script>
import { goto } from '$app/navigation';
import { login } from '$lib/auth';
let username = '';
let password = '';
let error = '';
let isLoading = false;
async function handleLogin() {
if (!username || !password) {
error = 'Please enter both username and password';
return;
}
isLoading = true;
error = '';
console.log("Sending request:", username, password);
// login request
try {
await login(username, password);
goto('/'); // Redirect after successful login
} catch (err) {
if (err instanceof Error) {
error = err.message || 'Invalid credentials';
} else {
error = 'Invalid credentials';
}
} finally {
isLoading = false;
}
}
</script>
<main class="container pt-3">
<div class="text-center mt-5 mb-4">
<img src="/logo-lg.svg" alt="ООО ЯКС" width="300" class="rounded-3" />
<h2 class="text-center mt-4 mb-5">Стратосферные полеты | ООО ЯКС</h2>
</div>
<div class="row">
<div class="col-12 col-md-6 col-lg-4 offset-md-3 offset-lg-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Вход в учетную запись</h5>
{#if error}
<div class="alert alert-danger mb-4" role="alert">{error}</div>
{/if}
<form on:submit|preventDefault={handleLogin} class="mt-4">
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
id="username"
placeholder="Имя пользователя"
bind:value={username}
required
/>
<label for="username">Имя пользователя</label>
</div>
<div class="form-floating mb-3">
<input
type="password"
class="form-control"
id="password"
placeholder="Пароль"
bind:value={password}
required
/>
<label for="password">Пароль</label>
</div>
<button
type="submit"
class="btn btn-primary w-100"
disabled={isLoading}
>
{#if isLoading}
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Вход...
{:else}
Войти
{/if}
</button>
<a href="/predict" class="btn btn-secondary mt-3 w-100">Назад</a>
</form>
</div>
</div>
</div>
</div>
</main>

View file

@ -0,0 +1 @@
export const ssr =false;

154
src/routes/map.svelte Normal file
View file

@ -0,0 +1,154 @@
<script lang="ts">
import { onMount, createEventDispatcher } from "svelte";
import * as L from "leaflet";
import type { Map as LeafletMap, LayerGroup } from "leaflet";
import "leaflet/dist/leaflet.css";
import WindVisualization from './WindVisualisation.svelte';
import { distHaversine } from "../lib/mathutil.ts";
import type { PredictionData, TelemetryData } from "../lib/types.ts";
/**
* @type {'prediction' | 'telemetry'}
*/
export let mode: "prediction" | "telemetry" = "prediction";
export let data: PredictionData | TelemetryData | null = null;
let map: typeof LeafletMap | undefined;
let mapContainer: HTMLDivElement;
let plotLayerGroup: typeof LayerGroup;
let mouseLat = 0;
let mouseLng = 0;
let isSelecting = false;
let windData;
const dispatch = createEventDispatcher<{ coordinatesSelected: { lat: number; lng: number } }>();
onMount(async () => {
if (!mapContainer) return;
map = L.map(mapContainer).setView([30, 0], 2);
plotLayerGroup = L.layerGroup().addTo(map);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
const response = await fetch("src/routes/testVelo.json");
windData = await response.json();
map.on("mousemove", (e: any) => {
mouseLat = e.latlng.lat;
mouseLng = e.latlng.lng;
});
map.on("click", (e: any) => {
if (isSelecting) {
dispatch("coordinatesSelected", { lat: e.latlng.lat, lng: e.latlng.lng });
stopSelection();
}
});
});
$: if (map && data) {
plotData(data);
} else if (map) {
clearMapLayers();
}
export const startSelection = () => {
isSelecting = true;
if (mapContainer) mapContainer.style.cursor = "crosshair";
};
export const stopSelection = () => {
isSelecting = false;
if (mapContainer) mapContainer.style.cursor = "";
};
export const plotData = (plotData: PredictionData | TelemetryData) => {
if (mode === "prediction") {
plotPrediction(plotData as PredictionData);
} else if (mode === "telemetry") {
plotTelemetry(plotData as TelemetryData);
}
};
export const clearMapLayers = () => {
plotLayerGroup?.clearLayers();
};
const launchIcon = L.icon({ iconUrl: "target-blue.png", iconSize: [10, 10], iconAnchor: [5, 5] });
const landIcon = L.icon({ iconUrl: "target-red.png", iconSize: [10, 10], iconAnchor: [5, 5] });
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 plotPrediction = (prediction: PredictionData) => {
const { launch, landing, burst, flight_path, flight_time } = prediction;
const range = distHaversine(launch.latlng, landing.latlng, 1);
const f_hours = Math.floor(flight_time / 3600);
const f_minutes = Math.floor((flight_time % 3600) / 60).toString().padStart(2, "0");
const flighttime = `${f_hours}hr${f_minutes}`;
L.marker(launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
L.marker(landing.latlng, { title: `Landing`, icon: landIcon }).addTo(plotLayerGroup);
L.marker(burst.latlng, { title: `Burst`, icon: burstIcon }).addTo(plotLayerGroup);
L.polyline(flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
map?.fitBounds(L.latLngBounds(flight_path));
};
const plotTelemetry = (telemetry: TelemetryData) => {
L.marker(telemetry.launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
telemetry.datapoints.forEach((point) => {
L.marker([point.latitude, point.longitude], {
title: `Telemetry at ${point.datetime}`,
icon: telemetryIcon,
})
.bindPopup(`<b>Telemetry Point</b><br>Lat: ${point.latitude.toFixed(6)}<br>Lon: ${point.longitude.toFixed(6)}`)
.addTo(plotLayerGroup);
});
L.polyline(telemetry.flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
map?.fitBounds(L.latLngBounds(telemetry.flight_path));
};
export const panTo = (lat: number, lng: number) => {
if (map) {
map.setView([lat, lng], map.getZoom());
}
};
export const zoomTo = (lat: number, lng: number, zoomLevel: number) => {
if (map) {
map.setView([lat, lng], zoomLevel);
}
};
export const getMap = () => {
return map;
};
</script>
<div class="map-container" bind:this={mapContainer}>
<div class="card coordinates-display">
<p class="card-text">
<b>Lat:</b>
{mouseLat.toFixed(6)},
<b>Lon:</b>
{mouseLng.toFixed(6)}
</p>
</div>
<div class="panel-container">
<slot />
</div>
{#if map && windData}
<WindVisualization {map} windData={windData} />
{/if}
</div>

View file

@ -0,0 +1,36 @@
<script lang="ts">
import Map from '../Map.svelte';
import ControlPanel from '../ControlPanel.svelte';
import Navbar from '../Navbar.svelte';
import { onMount } from 'svelte';
import { PredictionStore } from '$lib/stores';
import { Modal } from '@sveltestrap/sveltestrap';
import L from 'leaflet';
let map: Map | null = null;
let panel: ControlPanel | null = null;
onMount(() => {
PredictionStore.subscribe((data) => {
if (data) {
map?.clearMapLayers();
}
});
console.log('ControlPanel mounted');
console.log(panel);
if (panel) {
let element = panel.getElement();
L.DomEvent.disableClickPropagation(element);
L.DomEvent.disableScrollPropagation(element);
}
});
</script>
<main>
<Navbar />
<Map bind:this={map} mode="prediction" bind:data={$PredictionStore}>
<ControlPanel bind:this={panel} />
</Map>
</main>

View file

@ -0,0 +1 @@
export const ssr =false;

130432
src/routes/testVelo.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
<script>
import Map from '../Map.svelte';
import TelemetryPanel from '../TelemetryPanel.svelte';
import Navbar from '../Navbar.svelte';
// import BurstCalculator from './BurstCalculator.svelte';
let coordinates = {
lat: '56.3576',
lng: '39.8666'
}
</script>
<main>
<Navbar />
<Map>
<TelemetryPanel
/>
</Map>
</main>

View file

@ -0,0 +1 @@
export const ssr =false;

View file

@ -0,0 +1,12 @@
<script lang="ts">
import Navbar from '../../Navbar.svelte';
</script>
<main>
<Navbar />
<div class="container">
<h1>User Account</h1>
<p>Manage your account settings here.</p>
<!-- Add account management components or links here -->
</div>
</main>

View file

@ -0,0 +1 @@
export const ssr =false;

View file

@ -0,0 +1 @@
export const ssr =false;

View file

@ -0,0 +1 @@
export const ssr =false;

View file

@ -0,0 +1 @@
export const ssr = false;

7
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

83
static/css/custom.css Normal file
View file

@ -0,0 +1,83 @@
.custom-navbar {
height: var(--navbar-height);
padding-top: 0rem;
padding-bottom: 0rem;
z-index: 1000;
border: none;
background-color: white !important;
}
.nav-link {
color: inherit;
padding-left: 1rem !important;
padding-right: 1rem !important;
padding-top: 12px;
background-color: var(--bs-light);
margin-right: 1px;
}
.nav-link:hover {
color: white !important;;
background-color: var(--bs-primary);
}
.nav-link.active {
color: white !important;
background-color: var(--bs-primary);
}
.nav-full-height {
display: flex;
align-items: center;
height: 100%;
}
.white-bg {
background-color: #fff !important;
}
.navbar-brand {
margin-right: 1em;
}
.navbar {
z-index: 1001;
}
.card {
transition: all 0.3s ease;
}
.card-header {
cursor: pointer;
}
:root {
--navbar-height: 44px;
}
.map-container {
position: relative;
width: 100%;
height: calc(100vh - var(--navbar-height));
top: var(--navbar-height);
}
.coordinates-display {
position: absolute;
top: 10px;
right: 10px;
background: #fff;
padding: 3px 8px;
font-family: Arial, sans-serif;
font-size: 14px;
z-index: 1000;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
border: 1px solid #ccc;
width: auto;
white-space: nowrap;
}
.panel-container {
position: absolute;
bottom: 20px;
right: 20px;
z-index: 1000;
}

8
static/logo-lg.svg Normal file
View file

@ -0,0 +1,8 @@
<svg width="128" height="55" viewBox="0 0 128 55" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M127.629 26.5648H120.266C120.289 25.5875 120.164 24.7239 119.891 23.9739C119.618 23.2126 119.209 22.5649 118.664 22.0308C118.129 21.4967 117.476 21.0933 116.703 20.8206C115.931 20.5365 115.061 20.3945 114.095 20.3945C112.232 20.3945 110.522 20.8604 108.965 21.7922C107.408 22.724 106.096 24.0762 105.027 25.8489C103.959 27.6102 103.221 29.7409 102.812 32.2408C102.414 34.6498 102.437 36.6668 102.88 38.2918C103.323 39.9168 104.107 41.144 105.232 41.9735C106.368 42.7917 107.783 43.2008 109.476 43.2008C110.522 43.2008 111.516 43.0701 112.459 42.8088C113.402 42.536 114.26 42.1497 115.033 41.6497C115.817 41.1383 116.499 40.519 117.078 39.7918C117.669 39.0645 118.129 38.2407 118.459 37.3202H125.874C125.408 38.9225 124.68 40.4679 123.692 41.9565C122.715 43.4451 121.504 44.7746 120.061 45.945C118.618 47.1041 116.976 48.0245 115.135 48.7063C113.294 49.3882 111.277 49.7291 109.084 49.7291C105.914 49.7291 103.192 49.0018 100.92 47.5473C98.6583 46.0928 97.0276 43.9962 96.0277 41.2576C95.0277 38.5191 94.8402 35.218 95.4652 31.3545C96.0902 27.6159 97.3117 24.4455 99.1299 21.8433C100.959 19.2297 103.181 17.2468 105.795 15.8946C108.419 14.5423 111.226 13.8662 114.215 13.8662C116.294 13.8662 118.175 14.1503 119.857 14.7185C121.538 15.2866 122.97 16.1162 124.152 17.207C125.345 18.2866 126.243 19.6104 126.845 21.1786C127.447 22.7467 127.709 24.5421 127.629 26.5648Z" fill="#1E1E25"/>
<path d="M85.1998 49.2518L76.4557 34.3715H73.933L71.4615 49.2518H64.0809L69.8763 14.3435H77.2568L74.9386 28.2864H76.4216L90.3304 14.3435H99.5177L83.2226 30.5363L94.353 49.2518H85.1998Z" fill="#1E1E25"/>
<path d="M58.0299 49.2518H50.6494L55.4391 20.3774H50.4619C49.0415 20.3774 47.8256 20.5877 46.8143 21.0081C45.8143 21.4172 45.0132 22.0195 44.4109 22.8149C43.82 23.6103 43.428 24.5876 43.2348 25.7466C43.053 26.8944 43.1268 27.8546 43.4564 28.6273C43.7973 29.4 44.3995 29.9795 45.2632 30.3659C46.1381 30.7522 47.2802 30.9454 48.6892 30.9454H56.7004L55.7118 36.8771H46.4904C43.82 36.8771 41.6098 36.4339 39.8599 35.5476C38.1213 34.6612 36.8883 33.3885 36.1611 31.7295C35.4338 30.0591 35.2577 28.0648 35.6327 25.7466C36.0304 23.4399 36.8599 21.4342 38.1213 19.7297C39.394 18.0139 41.0417 16.69 43.0643 15.7582C45.087 14.8151 47.4165 14.3435 50.0528 14.3435H63.8082L58.0299 49.2518ZM41.8201 33.3658H49.7801L38.6497 49.2518H30.5192L41.8201 33.3658Z" fill="#1E1E25"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3903 21.7039C5.78668 34.0449 11.4416 47.2087 21.9106 54.1434C12.048 51.1317 2.7851 43.9135 0.832447 34.2409C-2.19369 19.2507 9.53957 4.23493 27.0394 0.702149C35.6391 -1.03391 43.7457 1.00909 50.4165 4.62037C42.6151 2.24532 34.2901 2.97345 27.1391 6.20747C26.0164 5.16499 24.5125 4.52753 22.8596 4.52753C19.3853 4.52753 16.5689 7.34398 16.5689 10.8183C16.5689 11.7004 16.7504 12.5401 17.0782 13.302C14.7667 15.7231 12.8274 18.5389 11.3903 21.7039Z" fill="#008DD2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.8595 17.109C26.3338 17.109 29.1502 14.2926 29.1502 10.8183C29.1502 10.1536 29.0472 9.51302 28.8561 8.91161C31.1779 7.85429 33.6924 7.08033 36.3547 6.64694C43.6811 5.45428 50.7637 7.06258 56.3383 10.6377C43.5445 5.75535 28.5474 12.1054 22.6323 25.0127C17.6132 35.9648 20.7246 48.165 29.4457 54.9259C21.1528 51.7528 14.8535 44.7886 13.4101 35.922C12.2177 28.5968 14.5801 21.452 19.3047 16.0091C20.316 16.703 21.5404 17.109 22.8595 17.109Z" fill="#009846"/>
<circle cx="22.8595" cy="10.8183" r="3.96366" transform="rotate(-0.650663 22.8595 10.8183)" fill="#C42526"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

28
static/logo.svg Normal file
View file

@ -0,0 +1,28 @@
<svg width="305" height="56" viewBox="0 0 305 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M127.296 26.7009H119.933C119.955 25.7237 119.83 24.8601 119.558 24.1101C119.285 23.3487 118.876 22.701 118.331 22.1669C117.796 21.6329 117.143 21.2295 116.37 20.9567C115.598 20.6727 114.728 20.5306 113.762 20.5306C111.899 20.5306 110.189 20.9965 108.632 21.9283C107.075 22.8601 105.763 24.2124 104.694 25.985C103.626 27.7464 102.888 29.877 102.479 32.3769C102.081 34.786 102.104 36.803 102.547 38.428C102.99 40.0529 103.774 41.2802 104.899 42.1097C106.035 42.9279 107.45 43.3369 109.143 43.3369C110.189 43.3369 111.183 43.2063 112.126 42.9449C113.069 42.6722 113.927 42.2858 114.7 41.7858C115.484 41.2745 116.166 40.6552 116.745 39.9279C117.336 39.2007 117.796 38.3768 118.126 37.4564H125.541C125.075 39.0586 124.347 40.604 123.359 42.0926C122.382 43.5812 121.171 44.9108 119.728 46.0812C118.285 47.2403 116.643 48.1607 114.802 48.8425C112.961 49.5243 110.944 49.8652 108.751 49.8652C105.581 49.8652 102.859 49.1379 100.587 47.6834C98.3253 46.2289 96.6946 44.1324 95.6947 41.3938C94.6947 38.6552 94.5072 35.3542 95.1322 31.4906C95.7572 27.7521 96.9787 24.5817 98.7969 21.9795C100.626 19.3659 102.848 17.383 105.461 16.0307C108.086 14.6785 110.893 14.0024 113.882 14.0024C115.961 14.0024 117.842 14.2864 119.524 14.8546C121.205 15.4228 122.637 16.2523 123.819 17.3432C125.012 18.4227 125.91 19.7465 126.512 21.3147C127.114 22.8828 127.376 24.6783 127.296 26.7009Z" fill="#1E1E25"/>
<path d="M84.8668 49.388L76.1227 34.5076H73.6L71.1284 49.388H63.7479L69.5433 14.4796H76.9238L74.6056 28.4225H76.0886L89.9973 14.4796H99.1846L82.8896 30.6725L94.02 49.388H84.8668Z" fill="#1E1E25"/>
<path d="M57.6969 49.388H50.3164L55.1061 20.5136H50.1289C48.7085 20.5136 47.4926 20.7238 46.4813 21.1443C45.4813 21.5533 44.6802 22.1556 44.0779 22.951C43.487 23.7465 43.095 24.7237 42.9018 25.8828C42.72 27.0305 42.7938 27.9907 43.1234 28.7634C43.4643 29.5361 44.0665 30.1157 44.9302 30.502C45.8051 30.8884 46.9472 31.0815 48.3562 31.0815H56.3674L55.3788 37.0132H46.1574C43.487 37.0132 41.2768 36.5701 39.5269 35.6837C37.7883 34.7974 36.5553 33.5247 35.8281 31.8656C35.1008 30.1952 34.9247 28.2009 35.2997 25.8828C35.6974 23.576 36.5269 21.5704 37.7883 19.8659C39.061 18.15 40.7087 16.8262 42.7313 15.8944C44.754 14.9512 47.0835 14.4796 49.7198 14.4796H63.4752L57.6969 49.388ZM41.487 33.5019H49.4471L38.3167 49.388H30.1862L41.487 33.5019Z" fill="#1E1E25"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0573 21.8401C5.45367 34.1811 11.1086 47.3449 21.5776 54.2795C11.715 51.2678 2.45209 44.0496 0.499439 34.3771C-2.5267 19.3869 9.20656 4.37107 26.7064 0.838284C35.3061 -0.897771 43.4127 1.14522 50.0835 4.75651C42.2821 2.38145 33.9571 3.10958 26.8061 6.3436C25.6834 5.30112 24.1795 4.66366 22.5266 4.66366C19.0523 4.66366 16.2359 7.48012 16.2359 10.9544C16.2359 11.8365 16.4174 12.6762 16.7452 13.4381C14.4337 15.8592 12.4944 18.6751 11.0573 21.8401Z" fill="#008DD2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5265 17.2452C26.0008 17.2452 28.8172 14.4287 28.8172 10.9544C28.8172 10.2898 28.7141 9.64916 28.5231 9.04774C30.8449 7.99042 33.3594 7.21647 36.0217 6.78307C43.348 5.59041 50.4307 7.19872 56.0053 10.7738C43.2114 5.89148 28.2144 12.2416 22.2993 25.1488C17.2802 36.1009 20.3916 48.3011 29.1127 55.062C20.8198 51.8889 14.5205 44.9248 13.0771 36.0581C11.8847 28.733 14.2471 21.5881 18.9717 16.1452C19.983 16.8391 21.2074 17.2452 22.5265 17.2452Z" fill="#009846"/>
<path d="M26.4899 10.9094C26.5148 13.0984 24.7605 14.893 22.5716 14.9178C20.3826 14.9427 18.588 13.1884 18.5631 10.9995C18.5383 8.81053 20.2926 7.0159 22.4815 6.99104C24.6705 6.96618 26.4651 8.72051 26.4899 10.9094Z" fill="#C42526"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M133.83 50.3023L133.83 13.2885L135.33 13.2885L135.33 50.3023L133.83 50.3023Z" fill="#008DD2"/>
<path d="M202.963 39.4941H205.708V43.8007H208.461C209.542 43.8007 210.366 43.8789 210.932 44.0351C211.505 44.1848 211.987 44.5169 212.377 45.0312C212.775 45.5455 212.973 46.151 212.973 46.8476C212.973 47.8502 212.628 48.6054 211.938 49.1132C211.248 49.6145 210.213 49.8652 208.833 49.8652H202.963V39.4941ZM205.708 48.1269H208.256C208.92 48.1269 209.402 48.0227 209.702 47.8144C210.001 47.6061 210.151 47.2675 210.151 46.7988C210.151 46.2975 209.956 45.9557 209.565 45.7734C209.181 45.5846 208.487 45.4902 207.485 45.4902H205.708V48.1269ZM214.536 39.4941H217.28V49.8652H214.536V39.4941Z" fill="#1E1E25"/>
<path d="M191.909 39.4941H201.293V41.7109H197.973V49.8652H195.229V41.7109H191.909V39.4941Z" fill="#1E1E25"/>
<path d="M188.286 46.5644L191.02 47.0234C190.668 48.026 190.112 48.791 189.35 49.3183C188.595 49.8391 187.648 50.0996 186.508 50.0996C184.705 50.0996 183.37 49.5104 182.504 48.332C181.821 47.388 181.479 46.1966 181.479 44.7578C181.479 43.039 181.928 41.6946 182.827 40.7246C183.725 39.748 184.861 39.2597 186.235 39.2597C187.778 39.2597 188.995 39.7708 189.887 40.7929C190.779 41.8085 191.206 43.3678 191.167 45.4707H184.292C184.311 46.2845 184.532 46.9192 184.956 47.3749C185.379 47.8242 185.906 48.0488 186.538 48.0488C186.967 48.0488 187.329 47.9316 187.622 47.6972C187.915 47.4628 188.136 47.0852 188.286 46.5644ZM188.442 43.791C188.422 42.9967 188.217 42.3945 187.827 41.9843C187.436 41.5677 186.961 41.3593 186.401 41.3593C185.802 41.3593 185.307 41.5774 184.917 42.0136C184.526 42.4498 184.334 43.0423 184.34 43.791H188.442Z" fill="#1E1E25"/>
<path d="M170.581 39.4941H179.77V49.8652H177.036V41.7207H173.295V46.3789C173.295 47.5638 173.159 48.3971 172.885 48.8789C172.612 49.3541 172.309 49.6666 171.977 49.8164C171.645 49.9661 171.111 50.041 170.375 50.041C169.939 50.041 169.363 49.9824 168.647 49.8652V47.8437C168.706 47.8437 168.859 47.8502 169.106 47.8632C169.392 47.8828 169.614 47.8925 169.77 47.8925C170.141 47.8925 170.369 47.7721 170.454 47.5312C170.538 47.2838 170.581 46.6035 170.581 45.4902V39.4941Z" fill="#1E1E25"/>
<path d="M157.485 44.5332C157.485 43.6217 157.709 42.7395 158.159 41.8867C158.608 41.0338 159.243 40.3828 160.063 39.9335C160.89 39.4843 161.811 39.2597 162.827 39.2597C164.396 39.2597 165.681 39.7708 166.684 40.7929C167.687 41.8085 168.188 43.0943 168.188 44.6503C168.188 46.2193 167.68 47.5214 166.665 48.5566C165.655 49.5852 164.383 50.0996 162.846 50.0996C161.896 50.0996 160.987 49.8847 160.122 49.455C159.262 49.0253 158.608 48.3971 158.159 47.5703C157.709 46.7369 157.485 45.7246 157.485 44.5332ZM160.297 44.6796C160.297 45.7083 160.542 46.496 161.03 47.0429C161.518 47.5898 162.12 47.8632 162.836 47.8632C163.553 47.8632 164.152 47.5898 164.633 47.0429C165.122 46.496 165.366 45.7018 165.366 44.6601C165.366 43.6445 165.122 42.8632 164.633 42.3164C164.152 41.7695 163.553 41.496 162.836 41.496C162.12 41.496 161.518 41.7695 161.03 42.3164C160.542 42.8632 160.297 43.651 160.297 44.6796Z" fill="#1E1E25"/>
<path d="M145.922 39.4941H155.131V49.8652H152.387V41.7109H148.666V49.8652H145.922V39.4941Z" fill="#1E1E25"/>
<path d="M302.036 24.5644L304.77 25.0234C304.418 26.026 303.862 26.791 303.1 27.3183C302.345 27.8391 301.398 28.0996 300.258 28.0996C298.455 28.0996 297.12 27.5104 296.254 26.332C295.571 25.388 295.229 24.1966 295.229 22.7578C295.229 21.039 295.678 19.6946 296.577 18.7246C297.475 17.748 298.611 17.2597 299.985 17.2597C301.528 17.2597 302.745 17.7708 303.637 18.7929C304.529 19.8085 304.956 21.3678 304.917 23.4707H298.042C298.061 24.2845 298.282 24.9192 298.706 25.3749C299.129 25.8242 299.656 26.0488 300.288 26.0488C300.717 26.0488 301.079 25.9316 301.372 25.6972C301.665 25.4628 301.886 25.0852 302.036 24.5644ZM302.192 21.791C302.172 20.9967 301.967 20.3945 301.577 19.9843C301.186 19.5677 300.711 19.3593 300.151 19.3593C299.552 19.3593 299.057 19.5774 298.667 20.0136C298.276 20.4498 298.084 21.0423 298.09 21.791H302.192Z" fill="#1E1E25"/>
<path d="M278.959 17.4941H281.704V21.8007H284.458C285.538 21.8007 286.362 21.8789 286.928 22.0351C287.501 22.1848 287.983 22.5169 288.374 23.0312C288.771 23.5455 288.969 24.151 288.969 24.8476C288.969 25.8502 288.624 26.6054 287.934 27.1132C287.244 27.6145 286.209 27.8652 284.829 27.8652H278.959V17.4941ZM281.704 26.1269H284.252C284.917 26.1269 285.398 26.0227 285.698 25.8144C285.997 25.6061 286.147 25.2675 286.147 24.7988C286.147 24.2975 285.952 23.9557 285.561 23.7734C285.177 23.5846 284.484 23.4902 283.481 23.4902H281.704V26.1269ZM290.532 17.4941H293.276V27.8652H290.532V17.4941Z" fill="#1E1E25"/>
<path d="M266.743 17.4941H269.487V21.2929H273.413V17.4941H276.167V27.8652H273.413V23.5097H269.487V27.8652H266.743V17.4941Z" fill="#1E1E25"/>
<path d="M254.545 17.4941H257.104V19.0175C257.436 18.4967 257.885 18.0735 258.452 17.748C259.018 17.4225 259.646 17.2597 260.336 17.2597C261.541 17.2597 262.563 17.7317 263.403 18.6757C264.243 19.6197 264.663 20.9348 264.663 22.621C264.663 24.3528 264.239 25.7005 263.393 26.664C262.547 27.621 261.521 28.0996 260.317 28.0996C259.744 28.0996 259.223 27.9856 258.754 27.7578C258.292 27.5299 257.804 27.1393 257.29 26.5859V31.8105H254.545V17.4941ZM257.26 22.5039C257.26 23.6692 257.491 24.5318 257.954 25.0917C258.416 25.6451 258.979 25.9218 259.643 25.9218C260.281 25.9218 260.812 25.6679 261.235 25.1601C261.658 24.6458 261.87 23.8059 261.87 22.6406C261.87 21.5533 261.652 20.746 261.215 20.2187C260.779 19.6914 260.239 19.4277 259.594 19.4277C258.924 19.4277 258.367 19.6881 257.924 20.2089C257.482 20.7233 257.26 21.4882 257.26 22.5039Z" fill="#1E1E25"/>
<path d="M249.497 24.5644L252.231 25.0234C251.879 26.026 251.323 26.791 250.561 27.3183C249.806 27.8391 248.859 28.0996 247.719 28.0996C245.916 28.0996 244.581 27.5104 243.715 26.332C243.032 25.388 242.69 24.1966 242.69 22.7578C242.69 21.039 243.139 19.6946 244.038 18.7246C244.936 17.748 246.072 17.2597 247.446 17.2597C248.989 17.2597 250.206 17.7708 251.098 18.7929C251.99 19.8085 252.417 21.3678 252.377 23.4707H245.502C245.522 24.2845 245.743 24.9192 246.167 25.3749C246.59 25.8242 247.117 26.0488 247.749 26.0488C248.178 26.0488 248.54 25.9316 248.833 25.6972C249.125 25.4628 249.347 25.0852 249.497 24.5644ZM249.653 21.791C249.633 20.9967 249.428 20.3945 249.038 19.9843C248.647 19.5677 248.172 19.3593 247.612 19.3593C247.013 19.3593 246.518 19.5774 246.127 20.0136C245.737 20.4498 245.545 21.0423 245.551 21.791H249.653Z" fill="#1E1E25"/>
<path d="M232.231 13.5488H234.956V18.8124C235.288 18.2981 235.685 17.914 236.147 17.6601C236.609 17.3997 237.137 17.2695 237.729 17.2695C238.842 17.2695 239.75 17.8098 240.454 18.8906C241.163 19.9648 241.518 21.2506 241.518 22.748C241.518 24.2519 241.121 25.5214 240.327 26.5566C239.539 27.5852 238.605 28.0996 237.524 28.0996C237.036 28.0996 236.577 27.9791 236.147 27.7382C235.717 27.4908 235.32 27.1263 234.956 26.6445V31.8105H232.231V26.6445C231.834 27.1263 231.407 27.4908 230.952 27.7382C230.502 27.9791 230.024 28.0996 229.516 28.0996C228.396 28.0996 227.472 27.582 226.743 26.5468C226.014 25.5117 225.649 24.2096 225.649 22.6406C225.649 21.0846 226.066 19.7988 226.899 18.7832C227.732 17.7675 228.634 17.2597 229.604 17.2597C230.131 17.2597 230.613 17.3834 231.049 17.6308C231.486 17.8782 231.879 18.2493 232.231 18.7441V13.5488ZM230.395 19.3593C229.887 19.3593 229.441 19.6653 229.057 20.2773C228.68 20.8828 228.491 21.6835 228.491 22.6796C228.491 23.6822 228.667 24.4863 229.018 25.0917C229.376 25.6972 229.819 25.9999 230.346 25.9999C230.88 25.9999 231.333 25.6972 231.704 25.0917C232.075 24.4863 232.26 23.6595 232.26 22.6113C232.26 21.472 232.058 20.6451 231.655 20.1308C231.251 19.6165 230.831 19.3593 230.395 19.3593ZM236.811 19.3789C236.251 19.3789 235.795 19.7076 235.444 20.3652C235.092 21.0162 234.917 21.8333 234.917 22.8164C234.917 23.7994 235.102 24.5807 235.473 25.1601C235.851 25.733 236.297 26.0195 236.811 26.0195C237.325 26.0195 237.768 25.694 238.139 25.0429C238.51 24.3919 238.696 23.6236 238.696 22.7382C238.696 21.7942 238.527 20.9999 238.188 20.3554C237.849 19.7044 237.39 19.3789 236.811 19.3789Z" fill="#1E1E25"/>
<path d="M224.174 20.5605L221.469 21.0488C221.378 20.5084 221.17 20.1015 220.844 19.8281C220.525 19.5546 220.109 19.4179 219.594 19.4179C218.911 19.4179 218.364 19.6555 217.954 20.1308C217.55 20.5996 217.348 21.3873 217.348 22.4941C217.348 23.7246 217.553 24.5937 217.963 25.1015C218.38 25.6093 218.937 25.8632 219.633 25.8632C220.154 25.8632 220.581 25.7167 220.913 25.4238C221.245 25.1243 221.479 24.6132 221.616 23.8906L224.311 24.3496C224.031 25.5865 223.494 26.5208 222.7 27.1523C221.905 27.7838 220.841 28.0996 219.506 28.0996C217.989 28.0996 216.778 27.621 215.874 26.664C214.975 25.707 214.526 24.3821 214.526 22.6894C214.526 20.9772 214.978 19.6458 215.883 18.6953C216.788 17.7382 218.012 17.2597 219.555 17.2597C220.818 17.2597 221.821 17.5332 222.563 18.08C223.312 18.6204 223.849 19.4472 224.174 20.5605Z" fill="#1E1E25"/>
<path d="M202.543 22.5332C202.543 21.6217 202.768 20.7395 203.217 19.8867C203.667 19.0338 204.301 18.3828 205.122 17.9335C205.948 17.4843 206.87 17.2597 207.885 17.2597C209.454 17.2597 210.74 17.7708 211.743 18.7929C212.745 19.8085 213.247 21.0943 213.247 22.6503C213.247 24.2193 212.739 25.5214 211.723 26.5566C210.714 27.5852 209.441 28.0996 207.905 28.0996C206.954 28.0996 206.046 27.8847 205.18 27.455C204.321 27.0253 203.667 26.3971 203.217 25.5703C202.768 24.7369 202.543 23.7246 202.543 22.5332ZM205.356 22.6796C205.356 23.7083 205.6 24.496 206.088 25.0429C206.577 25.5898 207.179 25.8632 207.895 25.8632C208.611 25.8632 209.21 25.5898 209.692 25.0429C210.18 24.496 210.424 23.7018 210.424 22.6601C210.424 21.6445 210.18 20.8632 209.692 20.3164C209.21 19.7695 208.611 19.496 207.895 19.496C207.179 19.496 206.577 19.7695 206.088 20.3164C205.6 20.8632 205.356 21.651 205.356 22.6796Z" fill="#1E1E25"/>
<path d="M192.417 17.4941H201.801V19.7109H198.481V27.8652H195.737V19.7109H192.417V17.4941Z" fill="#1E1E25"/>
<path d="M184.565 20.6582L182.075 20.2089C182.355 19.2063 182.836 18.4641 183.52 17.9824C184.204 17.5006 185.219 17.2597 186.567 17.2597C187.791 17.2597 188.702 17.4062 189.301 17.6992C189.9 17.9856 190.32 18.3535 190.561 18.8027C190.808 19.2454 190.932 20.0624 190.932 21.2539L190.903 24.457C190.903 25.3684 190.945 26.0423 191.03 26.4785C191.121 26.9082 191.287 27.3704 191.528 27.8652H188.813C188.741 27.6829 188.653 27.4127 188.549 27.0546C188.504 26.8919 188.471 26.7845 188.452 26.7324C187.983 27.1881 187.482 27.5299 186.948 27.7578C186.414 27.9856 185.844 28.0996 185.239 28.0996C184.171 28.0996 183.328 27.8098 182.709 27.2304C182.097 26.651 181.792 25.9186 181.792 25.0332C181.792 24.4472 181.931 23.9264 182.211 23.4707C182.491 23.0084 182.882 22.6568 183.383 22.416C183.891 22.1686 184.62 21.9537 185.571 21.7714C186.853 21.5305 187.742 21.3059 188.237 21.0976V20.8242C188.237 20.2968 188.107 19.9225 187.846 19.7011C187.586 19.4733 187.094 19.3593 186.372 19.3593C185.883 19.3593 185.502 19.457 185.229 19.6523C184.956 19.8411 184.734 20.1764 184.565 20.6582ZM188.237 22.8847C187.885 23.0019 187.329 23.1419 186.567 23.3046C185.805 23.4674 185.307 23.6269 185.073 23.7832C184.715 24.0371 184.536 24.3593 184.536 24.7499C184.536 25.1341 184.679 25.4661 184.965 25.746C185.252 26.026 185.616 26.166 186.059 26.166C186.554 26.166 187.026 26.0032 187.475 25.6777C187.807 25.4303 188.025 25.1276 188.129 24.7695C188.201 24.5351 188.237 24.0891 188.237 23.4316V22.8847Z" fill="#1E1E25"/>
<path d="M170.209 17.4941H172.768V19.0175C173.1 18.4967 173.549 18.0735 174.116 17.748C174.682 17.4225 175.31 17.2597 176 17.2597C177.205 17.2597 178.227 17.7317 179.067 18.6757C179.907 19.6197 180.327 20.9348 180.327 22.621C180.327 24.3528 179.903 25.7005 179.057 26.664C178.211 27.621 177.185 28.0996 175.981 28.0996C175.408 28.0996 174.887 27.9856 174.418 27.7578C173.956 27.5299 173.468 27.1393 172.954 26.5859V31.8105H170.209V17.4941ZM172.924 22.5039C172.924 23.6692 173.155 24.5318 173.618 25.0917C174.08 25.6451 174.643 25.9218 175.307 25.9218C175.945 25.9218 176.476 25.6679 176.899 25.1601C177.322 24.6458 177.534 23.8059 177.534 22.6406C177.534 21.5533 177.316 20.746 176.879 20.2187C176.443 19.6914 175.903 19.4277 175.258 19.4277C174.588 19.4277 174.031 19.6881 173.588 20.2089C173.146 20.7233 172.924 21.4882 172.924 22.5039Z" fill="#1E1E25"/>
<path d="M159.252 17.4941H168.637V19.7109H165.317V27.8652H162.573V19.7109H159.252V17.4941Z" fill="#1E1E25"/>
<path d="M155.209 22.6015L158.012 23.4902C157.582 25.0527 156.866 26.2148 155.864 26.9765C154.868 27.7317 153.601 28.1093 152.065 28.1093C150.164 28.1093 148.601 27.4615 147.377 26.166C146.153 24.8639 145.541 23.0865 145.541 20.8339C145.541 18.4511 146.157 16.6022 147.387 15.2871C148.618 13.9654 150.235 13.3046 152.241 13.3046C153.992 13.3046 155.414 13.8222 156.508 14.8574C157.159 15.4693 157.648 16.3483 157.973 17.4941L155.112 18.1777C154.942 17.4355 154.588 16.8496 154.047 16.4199C153.513 15.9902 152.862 15.7753 152.094 15.7753C151.033 15.7753 150.17 16.1562 149.506 16.9179C148.849 17.6796 148.52 18.9134 148.52 20.6191C148.52 22.429 148.845 23.718 149.497 24.4863C150.148 25.2545 150.994 25.6386 152.036 25.6386C152.804 25.6386 153.465 25.3945 154.018 24.9062C154.571 24.4179 154.969 23.6497 155.209 22.6015Z" fill="#1E1E25"/>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

BIN
static/marker-sm-red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

BIN
static/pop-marker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

BIN
static/target-blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
static/target-red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB