Add ruler tool and fix types

This commit is contained in:
ThePetrovich 2025-07-01 21:09:15 +08:00
parent 329c1c2215
commit bb390d50dc
16 changed files with 582 additions and 158 deletions

14
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@sveltestrap/sveltestrap": "^7.1.0",
"@types/leaflet": "^1.9.19",
"bootstrap-icons": "^1.13.1",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
@ -867,6 +868,19 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"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": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",

View file

@ -22,6 +22,7 @@
},
"dependencies": {
"@sveltestrap/sveltestrap": "^7.1.0",
"@types/leaflet": "^1.9.19",
"bootstrap-icons": "^1.13.1",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",

View file

@ -4,7 +4,10 @@
<meta charset="utf-8" />
<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-icons.css" />
<link rel="stylesheet" href="%sveltekit.assets%/ext/leaflet-ruler/leaflet-ruler.css" />
<link rel="stylesheet" href="%sveltekit.assets%/css/custom.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="stylesheet"

View file

@ -9,6 +9,7 @@
Input,
InputGroup,
InputGroupText,
Icon
} from "@sveltestrap/sveltestrap";
import { getForecast } from "$lib/prediction";
@ -130,37 +131,26 @@
<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}
<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>
<Icon name="caret-left-fill" class="text-white" />
{: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>
<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">
@ -181,18 +171,7 @@
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>
<Icon name="gear-fill" />
</Button>
</InputGroup>
</FormGroup>
@ -211,25 +190,7 @@
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>
<Icon name="journal-bookmark-fill" />
</Button>
</InputGroup>
</FormGroup>
@ -255,29 +216,29 @@
>
</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 w-50" 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" />
<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-0">
<Label for="startDate" class="form-label">Дата старта:</Label>
<Input type="date" id="startDate" class="form-control-sm" bind:value={startDate} />
<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-0">
<FormGroup class="flex-fill w-50" spacing="mb-2">
<Label for="ascentRate" class="form-label">Скорость подъема (м/с):</Label>
<Input
type="number"
@ -286,7 +247,7 @@
bind:value={$FlightParametersStore.ascent_rate}
/>
</FormGroup>
<FormGroup class="flex-fill w-50" spacing="mb-0">
<FormGroup class="flex-fill w-50" spacing="mb-2">
<Label for="descentRate" class="form-label">Скорость спуска (м/с):</Label>
<Input
type="number"
@ -297,16 +258,6 @@
</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="d-grid gap-1">
<Button color="outline-secondary" size="sm">Показать график высоты</Button>
<Button size="sm" color="primary" on:click={handleGetPrediction}>Выполнить прогнозирование</Button>

View file

@ -1,21 +1,19 @@
<script lang="ts">
import { onMount, createEventDispatcher } from "svelte";
import * as L from "leaflet";
import { ruler, Ruler } from "$lib/ext/leaflet-ruler/leaflet-ruler";
import type { Map as LeafletMap, LayerGroup } from "leaflet";
import "leaflet/dist/leaflet.css";
import WindVisualization from "$lib/components/WindVisualisation.svelte";
import { distHaversine } from "$lib/mathutil";
import type { PredictionData, TelemetryData } from "$lib/types";
import type { Prediction, Telemetry } from "$lib/types";
/**
* @type {'prediction' | 'telemetry'}
*/
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 plotLayerGroup: typeof LayerGroup;
let plotLayerGroup: LayerGroup;
let mouseLat = 0;
let mouseLng = 0;
let isSelecting = false;
@ -36,6 +34,10 @@
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
ruler({
position: "bottomright",
}).addTo(map);
const response = await fetch("src/routes/testVelo.json");
windData = await response.json();
@ -68,11 +70,11 @@
if (mapContainer) mapContainer.style.cursor = "";
};
export const plotData = (plotData: PredictionData | TelemetryData) => {
export const plotData = (plotData: Prediction | Telemetry) => {
if (mode === "prediction") {
plotPrediction(plotData as PredictionData);
plotPrediction(plotData as Prediction);
} else if (mode === "telemetry") {
plotTelemetry(plotData as TelemetryData);
plotTelemetry(plotData as Telemetry);
}
};
@ -85,7 +87,7 @@
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 plotPrediction = (prediction: Prediction) => {
const { launch, landing, burst, flight_path, flight_time } = prediction;
const range = distHaversine(launch.latlng, landing.latlng, 1);
@ -102,7 +104,7 @@
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);
telemetry.datapoints.forEach((point) => {

View file

@ -6,9 +6,6 @@
}
</script>
<div
bind:this={element}
class="panel-container"
>
<div bind:this={element} class="panel-container">
<slot />
</div>

View 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>

View 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: "&deg;",
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>&nbsp;${this._result.Bearing.toFixed(angleUnit.decimal)}&nbsp;${
angleUnit.display
}<br><b>${lengthUnit.label}</b>&nbsp;${this._totalLength.toFixed(lengthUnit.decimal)}&nbsp;${
lengthUnit.display
}`;
} else {
text = `<b>${angleUnit.label}</b>&nbsp;${this._result.Bearing.toFixed(angleUnit.decimal)}&nbsp;${
angleUnit.display
}<br><b>${lengthUnit.label}</b>&nbsp;${this._result.Distance.toFixed(lengthUnit.decimal)}&nbsp;${
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>&nbsp;${this._result.Bearing.toFixed(angleUnit.decimal)}&nbsp;${
angleUnit.display
}<br><b>${lengthUnit.label}</b>&nbsp;${this._addedLength.toFixed(lengthUnit.decimal)}&nbsp;${
lengthUnit.display
}<br><div class="plus-length">(+${this._result.Distance.toFixed(lengthUnit.decimal)})</div>`;
} else {
text = `<b>${angleUnit.label}</b>&nbsp;${this._result.Bearing.toFixed(angleUnit.decimal)}&nbsp;${
angleUnit.display
}<br><b>${lengthUnit.label}</b>&nbsp;${this._result.Distance.toFixed(lengthUnit.decimal)}&nbsp;${
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);
};

View file

@ -2,7 +2,7 @@ export function distHaversine(
p1: { lat: number; lng: number },
p2: { lat: number; lng: number },
precision?: number
): string {
): number {
const R = 6371; // Earth's mean radius in km
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 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;
}

View file

@ -23,6 +23,11 @@ export interface FlightParameters {
version: number;
}
export interface Point {
latlng: LatLngLiteral & { alt: number };
datetime: Date;
}
export interface TelemetryPoint {
altitude: number;
datetime: string;
@ -42,11 +47,8 @@ export interface RawTelemetry {
}
export interface Telemetry {
flight_path: [number, number, number][];
launch: {
latlng: LatLngExpression;
datetime: Date;
};
flight_path: LatLngExpression[];
launch: Point;
datapoints: TelemetryPoint[];
}
@ -74,38 +76,10 @@ export interface RawPrediction {
}
export interface Prediction {
flight_path: [number, number, number][];
launch: {
latlng: LatLngExpression;
datetime: Date;
};
burst: {
latlng: LatLngExpression;
datetime: Date;
};
landing: {
latlng: LatLngExpression;
datetime: Date;
};
flight_path: LatLngExpression[];
launch: Point;
burst: Point;
landing: Point;
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,5 +1,5 @@
<script>
import Navbar from './Navbar.svelte';
import Navbar from '$lib/components/Navbar.svelte';
</script>
<main>

View file

@ -4,6 +4,7 @@
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 { PredictionStore } from "$lib/stores";
@ -13,9 +14,10 @@
import L from "leaflet";
let map: Map | null = null;
let panel: PanelContainer | null = null;
let panelContainer: PanelContainer | null = null;
let controlPanel: ControlPanel | null = null;
let selectionToastId: string | null = null;
let activeTab: 'control' | 'telemetry' = 'control';
let activeTab: 'control' | 'scenario' | 'settings' | 'about' = 'scenario';
onMount(() => {
PredictionStore.subscribe((data) => {
@ -24,10 +26,11 @@
}
});
console.log("ControlPanel mounted");
console.log(panel);
console.log(panelContainer);
if (panel) {
let element = panel.getElement();
if (panelContainer) {
let element = panelContainer.getElement();
if (!element) return;
L.DomEvent.disableClickPropagation(element);
L.DomEvent.disableScrollPropagation(element);
}
@ -55,27 +58,23 @@
function handleCoordinateSelection(event: CustomEvent<{ lat: number; lng: number }>) {
const { lat, lng } = event.detail;
panel?.updateLaunchPosition(lat, lng);
controlPanel?.updateLaunchPosition(lat, lng);
console.log(`Selected coordinates: ${lat}, ${lng}`);
if (selectionToastId) {
removeToast(selectionToastId);
selectionToastId = null;
}
}
</script>
<main>
<Navbar />
<Map bind:this={map} mode="prediction" bind:data={$PredictionStore} on:coordinatesSelected={handleCoordinateSelection}>
<PanelContainer bind:this={panel}>
<PanelContainer bind:this={panelContainer} >
<TabComponent
tabs={[
{ id: 'control', icon: 'sliders', label: 'Прогноз' },
{ id: 'telemetry', icon: 'activity', label: 'Сценарий' },
{ id: 'scenario', icon: 'activity', label: 'Сценарий' },
{ id: 'control', icon: 'sliders', label: 'Условия' },
{ id: 'settings', icon: 'gear', label: 'Настройки' },
{ id: 'about', icon: 'info-circle', label: 'О проекте' }
]}
@ -84,9 +83,13 @@
<div>
{#if activeTab === 'control'}
<ControlPanel {handleClickSelectOnMap} />
{:else if activeTab === 'telemetry'}
<TelemetryPanel />
<ControlPanel {handleClickSelectOnMap} bind:this={controlPanel} />
{:else if activeTab === 'scenario'}
<ScenarioPanel />
{:else if activeTab === 'settings'}
<!-- <SettingsPanel /> -->
{:else if activeTab === 'about'}
<!-- <AboutPanel /> -->
{/if}
</div>
</PanelContainer>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import Navbar from '../../Navbar.svelte';
import Navbar from '$lib/components/Navbar.svelte';
</script>
<main>

View file

@ -92,6 +92,28 @@
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 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

View 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;
}