diff --git a/src/lib/components/Map.svelte b/src/lib/components/Map.svelte
index ff2ee78..5e6cc7a 100644
--- a/src/lib/components/Map.svelte
+++ b/src/lib/components/Map.svelte
@@ -1,21 +1,19 @@
-
+
-
\ No newline at end of file
+
diff --git a/src/lib/components/ScenarioPanel.svelte b/src/lib/components/ScenarioPanel.svelte
index e69de29..4791ced 100644
--- a/src/lib/components/ScenarioPanel.svelte
+++ b/src/lib/components/ScenarioPanel.svelte
@@ -0,0 +1,115 @@
+
+
+
+
+ {#if !isCollapsed}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/if}
+
diff --git a/src/lib/ext/leaflet-ruler/leaflet-ruler.ts b/src/lib/ext/leaflet-ruler/leaflet-ruler.ts
new file mode 100644
index 0000000..40ef2be
--- /dev/null
+++ b/src/lib/ext/leaflet-ruler/leaflet-ruler.ts
@@ -0,0 +1,286 @@
+import * as L from "leaflet";
+import { distHaversine, bearingHaversine } from "$lib/mathutil";
+
+// Define an interface for the control's options for type safety.
+export interface RulerOptions extends L.ControlOptions {
+ events?: {
+ onToggle?: (isActive: boolean) => void;
+ };
+ circleMarker?: L.CircleMarkerOptions;
+ lineStyle?: L.PolylineOptions;
+ lengthUnit?: {
+ display?: string;
+ decimal?: number;
+ factor?: number | null;
+ label?: string;
+ };
+ angleUnit?: {
+ display?: string;
+ decimal?: number;
+ factor?: number | null;
+ label?: string;
+ };
+}
+
+// Define an interface for the measurement result.
+interface MeasurementResult {
+ Bearing: number;
+ Distance: number;
+}
+
+// Use a modern TypeScript class that extends L.Control.
+export class Ruler extends L.Control {
+ // Override the default options with our custom ones.
+ public options: RulerOptions = {
+ position: "topright",
+ events: {
+ onToggle: () => {},
+ },
+ circleMarker: {
+ color: "red",
+ radius: 2,
+ },
+ lineStyle: {
+ color: "red",
+ dashArray: "1,6",
+ },
+ lengthUnit: {
+ display: "km",
+ decimal: 2,
+ factor: null,
+ label: "Distance:",
+ },
+ angleUnit: {
+ display: "°",
+ decimal: 2,
+ factor: null,
+ label: "Bearing:",
+ },
+ };
+
+ // Declare class properties with types.
+ private _lastClickTime = 0;
+ private _map?: L.Map;
+ private _container?: HTMLElement;
+ private _choice = false;
+ private _defaultCursor = "";
+ private _allLayers: L.LayerGroup = L.layerGroup();
+ private _clickedLatLong: L.LatLng | null = null;
+ private _clickedPoints: L.LatLng[] = [];
+ private _totalLength = 0;
+ private _clickCount = 0;
+ private _tempLine: L.FeatureGroup = L.featureGroup();
+ private _tempPoint: L.FeatureGroup = L.featureGroup();
+ private _pointLayer: L.FeatureGroup = L.featureGroup();
+ private _polylineLayer: L.FeatureGroup = L.featureGroup();
+ private _movingLatLong: L.LatLng | null = null;
+ private _result: MeasurementResult = { Bearing: 0, Distance: 0 };
+ private _addedLength = 0;
+
+ constructor(options?: RulerOptions) {
+ super(options);
+ L.Util.setOptions(this, options);
+ }
+
+ public isActive(): boolean {
+ return this._choice;
+ }
+
+ public onAdd(map: L.Map): HTMLElement {
+ this._map = map;
+ this._container = L.DomUtil.create("div", "leaflet-bar leaflet-ruler");
+ L.DomEvent.disableClickPropagation(this._container);
+ L.DomEvent.on(this._container, "click", this._toggleMeasure, this);
+ this._defaultCursor = this._map.getContainer().style.cursor;
+ this._allLayers = L.layerGroup();
+ return this._container;
+ }
+
+ public onRemove(): void {
+ if (this._container) {
+ L.DomEvent.off(this._container, "click", this._toggleMeasure, this);
+ }
+ if (this._choice) {
+ this._toggleMeasure(); // Turn off measurements
+ }
+ }
+
+ private _toggleMeasure(): void {
+ this._choice = !this._choice;
+ this.options.events?.onToggle?.(this._choice);
+
+ this._clickedLatLong = null;
+ this._clickedPoints = [];
+ this._totalLength = 0;
+
+ if (!this._map || !this._container) return;
+
+ const mapContainer = this._map.getContainer();
+
+ if (this._choice) {
+ this._map.doubleClickZoom.disable();
+ L.DomEvent.on(mapContainer, "keydown", this._escape, this);
+ L.DomEvent.on(mapContainer, "dblclick", this._closePath, this);
+ this._container.classList.add("leaflet-ruler-clicked");
+ this._clickCount = 0;
+ this._tempLine = L.featureGroup().addTo(this._allLayers);
+ this._tempPoint = L.featureGroup().addTo(this._allLayers);
+ this._pointLayer = L.featureGroup().addTo(this._allLayers);
+ this._polylineLayer = L.featureGroup().addTo(this._allLayers);
+ this._allLayers.addTo(this._map);
+ mapContainer.style.cursor = "crosshair";
+ this._map.on("click", this._clicked, this);
+ this._map.on("mousemove", this._moving, this);
+ } else {
+ this._map.doubleClickZoom.enable();
+ L.DomEvent.off(mapContainer, "keydown", this._escape, this);
+ L.DomEvent.off(mapContainer, "dblclick", this._closePath, this);
+ this._container.classList.remove("leaflet-ruler-clicked");
+ this._map.removeLayer(this._allLayers);
+ this._allLayers = L.layerGroup();
+ mapContainer.style.cursor = this._defaultCursor;
+ this._map.off("click", this._clicked, this);
+ this._map.off("mousemove", this._moving, this);
+ }
+ }
+
+ private _clicked(e: L.LeafletMouseEvent): void {
+ // hack to prevent adding the same point twice on double click
+ let clickTime = Date.now();
+ if (clickTime - this._lastClickTime < 200) {
+ this._closePath();
+ return;
+ }
+
+ this._lastClickTime = clickTime;
+
+ this._clickedLatLong = e.latlng;
+ this._clickedPoints.push(this._clickedLatLong);
+ L.circleMarker(this._clickedLatLong, this.options.circleMarker).addTo(this._pointLayer);
+
+ if (this._clickCount > 0 && !e.latlng.equals(this._clickedPoints[this._clickedPoints.length - 2], 0.0001)) {
+ if (this._movingLatLong) {
+ L.polyline(
+ [this._clickedPoints[this._clickCount - 1], this._movingLatLong],
+ this.options.lineStyle
+ ).addTo(this._polylineLayer);
+ }
+ let text: string;
+ this._totalLength += this._result.Distance;
+ const angleUnit = this.options.angleUnit!;
+ const lengthUnit = this.options.lengthUnit!;
+
+ if (this._clickCount > 1) {
+ text = `
${angleUnit.label} ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
+ angleUnit.display
+ }
${lengthUnit.label} ${this._totalLength.toFixed(lengthUnit.decimal)} ${
+ lengthUnit.display
+ }`;
+ } else {
+ text = `
${angleUnit.label} ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
+ angleUnit.display
+ }
${lengthUnit.label} ${this._result.Distance.toFixed(lengthUnit.decimal)} ${
+ lengthUnit.display
+ }`;
+ }
+ L.circleMarker(this._clickedLatLong, this.options.circleMarker)
+ .bindTooltip(text, { permanent: true, className: "result-tooltip" })
+ .addTo(this._pointLayer)
+ .openTooltip();
+ }
+ this._clickCount++;
+ }
+
+ private _moving(e: L.LeafletMouseEvent): void {
+ if (this._clickedLatLong && this._map) {
+ this._movingLatLong = e.latlng;
+
+ this._tempLine.clearLayers();
+ this._tempPoint.clearLayers();
+
+ this._calculateBearingAndDistance();
+ this._addedLength = this._result.Distance + this._totalLength;
+
+ L.polyline([this._clickedLatLong, this._movingLatLong], this.options.lineStyle).addTo(this._tempLine);
+
+ const angleUnit = this.options.angleUnit!;
+ const lengthUnit = this.options.lengthUnit!;
+ let text: string;
+
+ if (this._clickCount > 1) {
+ text = `
${angleUnit.label} ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
+ angleUnit.display
+ }
${lengthUnit.label} ${this._addedLength.toFixed(lengthUnit.decimal)} ${
+ lengthUnit.display
+ }
(+${this._result.Distance.toFixed(lengthUnit.decimal)})
`;
+ } else {
+ text = `
${angleUnit.label} ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
+ angleUnit.display
+ }
${lengthUnit.label} ${this._result.Distance.toFixed(lengthUnit.decimal)} ${
+ lengthUnit.display
+ }`;
+ }
+ L.circleMarker(this._movingLatLong, this.options.circleMarker)
+ .bindTooltip(text, { sticky: true, offset: L.point(0, -40), className: "moving-tooltip" })
+ .addTo(this._tempPoint)
+ .openTooltip();
+ }
+ }
+
+ private _escape(e: Event): void {
+ if ((e as KeyboardEvent).key === "Escape") {
+ if (this._clickCount > 0) {
+ this._closePath();
+ } else {
+ this._toggleMeasure();
+ }
+ }
+ }
+
+ private _calculateBearingAndDistance(): void {
+ if (!this._clickedLatLong || !this._movingLatLong) return;
+
+ const f1 = this._clickedLatLong.lat;
+ const l1 = this._clickedLatLong.lng;
+ const f2 = this._movingLatLong.lat;
+ const l2 = this._movingLatLong.lng;
+
+ const angleUnit = this.options.angleUnit!;
+ const lengthUnit = this.options.lengthUnit!;
+
+ const brng = bearingHaversine({ lat: f1, lng: l1 }, { lat: f2, lng: l2 });
+
+ const distance = distHaversine({ lat: f1, lng: l1 }, { lat: f2, lng: l2 });
+
+ if (angleUnit.factor) {
+ this._result.Bearing = brng * angleUnit.factor;
+ } else {
+ this._result.Bearing = brng;
+ }
+
+ if (lengthUnit.factor) {
+ this._result.Distance = distance * lengthUnit.factor;
+ } else {
+ this._result.Distance = distance;
+ }
+
+ this._result = {
+ Bearing: brng,
+ Distance: distance,
+ };
+ }
+
+ private _closePath(): void {
+ if (!this._map || !this._container) return;
+
+ this._map.removeLayer(this._tempLine);
+ this._map.removeLayer(this._tempPoint);
+ this._choice = false;
+ this._toggleMeasure();
+ }
+}
+
+// Factory function for creating the control, maintaining the Leaflet convention.
+export const ruler = (options?: RulerOptions) => {
+ return new Ruler(options);
+};
diff --git a/src/lib/mathutil.ts b/src/lib/mathutil.ts
index 75bcebe..eee4bac 100644
--- a/src/lib/mathutil.ts
+++ b/src/lib/mathutil.ts
@@ -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;
}
\ No newline at end of file
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 34029d5..388f8d0 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -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[];
-}
\ No newline at end of file
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index f961cbf..cbb97e2 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,5 +1,5 @@
diff --git a/src/routes/predict/+page.svelte b/src/routes/predict/+page.svelte
index 667c46e..62fc819 100644
--- a/src/routes/predict/+page.svelte
+++ b/src/routes/predict/+page.svelte
@@ -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;
}
}
-
-
-
-
diff --git a/src/routes/user/account/+page.svelte b/src/routes/user/account/+page.svelte
index 9d97fbd..f8ba827 100644
--- a/src/routes/user/account/+page.svelte
+++ b/src/routes/user/account/+page.svelte
@@ -1,5 +1,5 @@