548 lines
No EOL
13 KiB
Svelte
548 lines
No EOL
13 KiB
Svelte
<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: '© <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} m³</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> |