diff --git a/.dockerignore b/.dockerignore
index 76b800d..fcfd3ac 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -11,3 +11,5 @@
.nuget-packages
Petabit/docs
Petabit.Tests
+**/App_Data
+artifacts
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 971838a..7c0f214 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,6 +40,15 @@ jobs:
- name: Run tests
run: dotnet test Petabit.sln --configuration Release --no-build --verbosity normal
+ - name: Set up Node.js
+ uses: actions/setup-node@v7
+ with:
+ node-version: 24
+ package-manager-cache: false
+
+ - name: Test ISS video playback
+ run: node --test Petabit.Tests/*.test.cjs Petabit.Tests/*.test.mjs
+
docker-build:
name: Build Docker image
runs-on: ubuntu-latest
diff --git a/.github/workflows/station-status-freshness.yml b/.github/workflows/station-status-freshness.yml
index d585a89..b10d359 100644
--- a/.github/workflows/station-status-freshness.yml
+++ b/.github/workflows/station-status-freshness.yml
@@ -9,7 +9,7 @@ on:
- "Petabit.Tests/StationStatusTests.cs"
- ".github/workflows/station-status-freshness.yml"
schedule:
- - cron: "0 7 * * 1"
+ - cron: "0 * * * *"
workflow_dispatch:
permissions:
@@ -17,21 +17,22 @@ permissions:
jobs:
verify-station-status:
- name: Verify curated ISS data age
+ name: Verify live NASA synchronization
runs-on: ubuntu-latest
- timeout-minutes: 10
-
+ timeout-minutes: 3
steps:
- - name: Check out repository
- uses: actions/checkout@v7
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v6
- with:
- dotnet-version: 10.0.x
-
- - name: Restore dependencies
- run: dotnet restore Petabit.sln --configfile NuGet.Config
-
- - name: Check station-status freshness
- run: dotnet test Petabit.Tests/Petabit.Tests.csproj --configuration Release --no-restore --filter "Category=StationStatusFreshness"
+ - name: Check production station status
+ env:
+ SITE_URL: https://petabit-production.up.railway.app
+ run: |
+ curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
+ "$SITE_URL/Home/StationData" |
+ jq --exit-status '
+ .stationStatusCheckedAt != null and
+ (.stationStatusIsStale == false) and
+ (.stationStatusRefreshFailed == false) and
+ (.stationStatusNeedsReview == false) and
+ (.stationStatusPersistenceFailed == false) and
+ (.astronautCount == (.astronauts | length)) and
+ ((.dockedVehicles | length) > 0)
+ '
diff --git a/.gitignore b/.gitignore
index f6acd36..69c73d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -363,3 +363,4 @@ MigrationBackup/
FodyWeavers.xsd
# Local NuGet state used by isolated builds.
.nuget-appdata/
+**/App_Data/
diff --git a/Petabit.Tests/ApplicationTests.cs b/Petabit.Tests/ApplicationTests.cs
index e8d4639..ffc370d 100644
--- a/Petabit.Tests/ApplicationTests.cs
+++ b/Petabit.Tests/ApplicationTests.cs
@@ -25,6 +25,7 @@ public ApplicationTests(WebApplicationFactory factory)
builder.ConfigureLogging(logging => logging.ClearProviders());
builder.ConfigureServices(services =>
{
+ services.Configure(options => options.Enabled = false);
services.AddDataProtection().UseEphemeralDataProtectionProvider();
services.RemoveAll();
services.AddSingleton(
@@ -418,6 +419,19 @@ public async Task GlobalRateLimitProtectsDynamicPagesButNotLiveness()
private HttpClient CreateClientWithIssResponse(HttpStatusCode statusCode, string content)
=> CreateClientWithHandler(new StubHttpMessageHandler(statusCode, content));
+ [Fact]
+ public async Task StationStatusRemainsAvailableWhenPositionApiFails()
+ {
+ using var client = CreateClientWithIssResponse(HttpStatusCode.ServiceUnavailable, "Unavailable");
+ var response = await client.GetAsync("/Home/StationData");
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains("\"astronautCount\":11", body);
+ Assert.Contains("Crew-13 Dragon", body);
+ Assert.Contains("\"stationStatusIsStale\":true", body);
+ Assert.Equal("no-store,no-cache", response.Headers.CacheControl?.ToString()?.Replace(" ", ""));
+ }
+
private HttpClient CreateClientWithHandler(HttpMessageHandler handler)
{
return new WebApplicationFactory()
@@ -426,6 +440,7 @@ private HttpClient CreateClientWithHandler(HttpMessageHandler handler)
builder.ConfigureLogging(logging => logging.ClearProviders());
builder.ConfigureServices(services =>
{
+ services.Configure(options => options.Enabled = false);
services.AddDataProtection().UseEphemeralDataProtectionProvider();
services.RemoveAll();
services.AddSingleton(
diff --git a/Petabit.Tests/NasaLiveVideoTests.cs b/Petabit.Tests/NasaLiveVideoTests.cs
new file mode 100644
index 0000000..3aaa470
--- /dev/null
+++ b/Petabit.Tests/NasaLiveVideoTests.cs
@@ -0,0 +1,25 @@
+using Petabit.Services;
+using Xunit;
+
+namespace Petabit.Tests;
+
+public class NasaLiveVideoTests
+{
+ [Fact]
+ public void CameraIdComesFromNasaEmbedRatherThanOtherPageLinks()
+ {
+ var html = """
+ Channel
+ VIDEO
+ """;
+ Assert.Equal("awQzjn72bI0", NasaLiveVideoService.ParseVideoId(html));
+ }
+
+ [Theory]
+ [InlineData("VIDEO ")]
+ [InlineData("")]
+ [InlineData("VIDEO ")]
+ [InlineData("Not a camera ")]
+ public void UnsupportedOrUnexpectedSourceIsRejected(string html)
+ => Assert.Throws(() => NasaLiveVideoService.ParseVideoId(html));
+}
diff --git a/Petabit.Tests/NasaStationParserTests.cs b/Petabit.Tests/NasaStationParserTests.cs
new file mode 100644
index 0000000..71af4ff
--- /dev/null
+++ b/Petabit.Tests/NasaStationParserTests.cs
@@ -0,0 +1,123 @@
+using Petabit.Models;
+using Petabit.Services;
+using Xunit;
+
+namespace Petabit.Tests;
+
+public class NasaStationParserTests
+{
+ private static NasaArticle Article(string title, string text) => new(
+ "test", title, "https://www.nasa.gov/blogs/spacestation/test/",
+ StationStatus.LastVerified.AddDays(5), text);
+
+ [Fact]
+ public void ConfigurationUsesDatedCaptionInsteadOfOutdatedPageCounter()
+ {
+ var observation = NasaStationParser.ParseVehicles("""
+ TODAY: 4 spacecraft
+ Oct. 1, 2026: International Space Station Configuration. Six spaceships are attached to the space station including the SpaceX Crew-12 and Crew-13 Dragons, Northrop Grumman’s Cygnus XL, the Soyuz MS-29 crew spacecraft, and the Progress 95 and 96 resupply ships.
+
+ """);
+ Assert.Equal(6, observation.Vehicles.Length);
+ Assert.Contains(observation.Vehicles, v => v.Name == "Crew-13 Dragon");
+ Assert.Contains(observation.Vehicles, v => v.Name == "Progress 96");
+ Assert.Equal(new DateTimeOffset(2026, 10, 1, 0, 0, 0, TimeSpan.Zero), observation.Date);
+ }
+
+ [Fact]
+ public void UnknownVehicleDoesNotProduceAnIncompleteConfiguration()
+ => Assert.Throws(() => NasaStationParser.ParseVehicles("""
+ Oct. 1, 2026: International Space Station Configuration. Two spaceships are attached including Crew-13 Dragon and a new spacecraft.
+ """));
+
+ [Theory]
+ [InlineData("NASA, SpaceX Set No Earlier Than Oct. 7 for Crew-12 Departure", "Crew-12 is scheduled to undock tomorrow.")]
+ [InlineData("Crew-12 Prepares to Depart", "Crew-12 will depart tomorrow.")]
+ [InlineData("Crew-14 Approaching Station", "Crew-14 is scheduled to dock tomorrow.")]
+ public void AnnouncementsDoNotChangeStatus(string title, string text)
+ {
+ var initial = StationSnapshot.Bootstrap();
+ var result = NasaStationParser.ApplyArticle(initial, Article(title, text));
+ Assert.Equal(initial, result.Snapshot);
+ Assert.Equal("news", result.Kind);
+ }
+
+ [Fact]
+ public void UndockingRemovesOnlyTheDepartingMissionAndItsCrew()
+ {
+ var result = NasaStationParser.ApplyArticle(StationSnapshot.Bootstrap(), Article(
+ "Crew-12 Dragon Undocks from Station", "The Crew-12 Dragon undocked from the International Space Station at 1:05 a.m. EDT."));
+ Assert.Equal("departure", result.Kind);
+ Assert.Equal(7, result.Snapshot.Crew.Length);
+ Assert.Equal(5, result.Snapshot.DockedVehicles.Length);
+ Assert.DoesNotContain(result.Snapshot.Crew, c => c.Mission == "Crew-12");
+ Assert.Contains(result.Snapshot.DockedVehicles, v => v.Name == "Crew-13 Dragon");
+ }
+
+ [Fact]
+ public void DockingDoesNotAddCrewBeforeHatchOpening()
+ {
+ var result = NasaStationParser.ApplyArticle(StationSnapshot.Bootstrap(), Article(
+ "Dragon Docks Bringing SpaceX Crew-14 to Station", "The Crew-14 Dragon docked to the station at 7:05 p.m. EDT."));
+ Assert.Equal("arrival", result.Kind);
+ Assert.Equal(11, result.Snapshot.Crew.Length);
+ Assert.Equal(7, result.Snapshot.DockedVehicles.Length);
+ }
+
+ [Fact]
+ public void HatchOpeningAddsTheNamedCrewWithAgencyAndMission()
+ {
+ var result = NasaStationParser.ApplyArticle(StationSnapshot.Bootstrap(), Article(
+ "Crew-14 Members Enter Station, Join Expedition 76",
+ "NASA astronauts Alice Smith and Bob Jones, CSA (Canadian Space Agency) astronaut Carol White, and Roscosmos cosmonaut Dmitry Petrov have entered the International Space Station following hatch opening."));
+ Assert.Equal("crew-arrival", result.Kind);
+ Assert.Equal(15, result.Snapshot.Crew.Length);
+ Assert.Contains(result.Snapshot.Crew, c => c.Name == "Carol White" && c.Agency == "CSA" && c.Mission == "Crew-14");
+ }
+
+ [Fact]
+ public void UnclearCompletedEventKeepsPreviousStatusAndRequestsReview()
+ {
+ var initial = StationSnapshot.Bootstrap();
+ var result = NasaStationParser.ApplyArticle(initial, Article("Dragon Undocks from Station", "Dragon undocked from the station."));
+ Assert.True(result.Snapshot.NeedsReview);
+ Assert.Equal(initial.Crew, result.Snapshot.Crew);
+ Assert.Equal(initial.DockedVehicles, result.Snapshot.DockedVehicles);
+ }
+
+ [Fact]
+ public void FeedIgnoresHistoricalImageCaptionsAndUsesUtcPublicationTime()
+ {
+ var articles = NasaStationParser.ParseFeed("""
+ -
+
Crew-12 Prepares to Depart https://www.nasa.gov/blogs/spacestation/test/
+ Sat, 03 Oct 2026 19:19:36 +0000
+ Dragon undocked last year.Departure is planned.
]]>
+
+ """);
+ Assert.Equal("Departure is planned.", Assert.Single(articles).Text);
+ Assert.Equal(TimeSpan.Zero, articles[0].PublishedAt.Offset);
+ }
+
+ [Fact]
+ public void FeedRejectsNonNasaLinks()
+ => Assert.Throws(() => NasaStationParser.ParseFeed("""
+ https://attacker.example/
+ """));
+
+ [Fact]
+ public void FeedRejectsExternalXmlEntities()
+ => Assert.Throws(() => NasaStationParser.ParseFeed("""
+ ]>&external;
+ """));
+
+ [Fact]
+ public void NewerVehicleDiagramDoesNotPreventConfirmedCrewDeparture()
+ {
+ var initial = StationSnapshot.Bootstrap() with { VehiclesUpdatedAt = StationStatus.LastVerified.AddDays(6) };
+ var result = NasaStationParser.ApplyArticle(initial, Article("Crew-12 Dragon Undocks from Station",
+ "The Crew-12 Dragon undocked from the International Space Station."));
+ Assert.Equal(7, result.Snapshot.Crew.Length);
+ Assert.Equal(initial.DockedVehicles, result.Snapshot.DockedVehicles);
+ }
+}
diff --git a/Petabit.Tests/StarlinkTests.cs b/Petabit.Tests/StarlinkTests.cs
new file mode 100644
index 0000000..ff8d933
--- /dev/null
+++ b/Petabit.Tests/StarlinkTests.cs
@@ -0,0 +1,108 @@
+using System.Text.Json;
+using System.Net;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging.Abstractions;
+using Petabit.Services;
+using Xunit;
+
+namespace Petabit.Tests;
+
+public class StarlinkTests : IDisposable
+{
+ private readonly string directory = Path.Combine(Path.GetTempPath(), "petabit-starlink-tests", Guid.NewGuid().ToString("N"));
+ private static readonly DateTimeOffset Now = DateTimeOffset.Parse("2026-10-05T00:00:00Z");
+ private static object Catalog(int id, string status, string decay = "", string type = "ORB") => new
+ {
+ OBJECT_NAME = $"STARLINK-{id}", NORAD_CAT_ID = id, OBJECT_TYPE = "PAY", OPS_STATUS_CODE = status,
+ ORBIT_CENTER = "EA", ORBIT_TYPE = type, DECAY_DATE = decay
+ };
+ private static object Orbit(int id, double eccentricity = .0002923) => new
+ {
+ OBJECT_NAME = $"STARLINK-{id}", NORAD_CAT_ID = id, EPOCH = "2026-10-04T22:36:12.73824",
+ MEAN_MOTION = 15.66040205, ECCENTRICITY = eccentricity, INCLINATION = 53.1448,
+ RA_OF_ASC_NODE = 256.2279, ARG_OF_PERICENTER = 92.7295, MEAN_ANOMALY = 267.4056,
+ BSTAR = .00047264149, MEAN_MOTION_DOT = .00049034, MEAN_MOTION_DDOT = 0
+ };
+ [Fact]
+ public void OperationalIsStrictlyPlusNotAllActiveAndDecayedObjectsAreExcluded()
+ {
+ var catalog = JsonSerializer.Serialize(new[] { Catalog(100800, "+"), Catalog(2, "P"), Catalog(3, "S"),
+ Catalog(4, "-"), Catalog(5, "D", "2026-10-01"), Catalog(6, "+", type: "IMP"), Catalog(7, "+") });
+ var state = StarlinkService.Parse(catalog, JsonSerializer.Serialize(new[] { Orbit(100800), Orbit(2), Orbit(100800) }), Now);
+ Assert.Equal(2, state.OperationalCount);
+ Assert.Equal(5, state.OnOrbitCount);
+ Assert.Equal(1, state.PartiallyOperationalCount);
+ Assert.Single(state.Elements);
+ Assert.Equal(100800, state.Elements[0].GetProperty("NORAD_CAT_ID").GetInt32());
+ Assert.Equal(DateTimeOffset.Parse("2026-10-04T22:36:12.73824Z"), state.OldestEpoch);
+ }
+ [Theory]
+ [InlineData("[]", "[]")]
+ [InlineData("{}", "[]")]
+ public void EmptyOrUnexpectedResponsesAreRejected(string catalog, string elements)
+ => Assert.Throws(() => StarlinkService.Parse(catalog, elements, Now));
+ [Fact]
+ public void InvalidOrbitIsRejectedRatherThanReplacingLastGoodSnapshot()
+ => Assert.Throws(() => StarlinkService.Parse(JsonSerializer.Serialize(new[] { Catalog(1, "+") }),
+ JsonSerializer.Serialize(new[] { Orbit(1, 2) }), Now));
+
+ private StarlinkService Create(Handler handler) => new(new Factory(handler), NullLogger.Instance,
+ new EnvironmentStub { ContentRootPath = directory });
+
+ [Fact]
+ public async Task RepeatedPingAndRestartReuseThePersistedTwoHourCache()
+ {
+ var handler = new Handler();
+ var service = Create(handler);
+ var first = await service.GetAsync(CancellationToken.None);
+ Assert.Same(first, await service.GetAsync(CancellationToken.None));
+ Assert.Equal(2, handler.Requests);
+ var restarted = Create(handler);
+ Assert.Equal(first.OperationalCount, (await restarted.GetAsync(CancellationToken.None)).OperationalCount);
+ Assert.Equal(2, handler.Requests);
+ }
+
+ [Fact]
+ public async Task FailedRefreshKeepsLastGoodCountAndDoesNotRepeatedlyDownload()
+ {
+ Directory.CreateDirectory(Path.Combine(directory, "App_Data"));
+ var snapshot = StarlinkService.Parse(JsonSerializer.Serialize(new[] { Catalog(1, "+") }),
+ JsonSerializer.Serialize(new[] { Orbit(1) }), Now) with { RetrievedAt = DateTimeOffset.UtcNow.AddHours(-3) };
+ await File.WriteAllTextAsync(Path.Combine(directory, "App_Data", "starlink-status.json"), JsonSerializer.Serialize(snapshot));
+ var handler = new Handler { Unavailable = true };
+ var service = Create(handler);
+ var result = await service.GetAsync(CancellationToken.None);
+ Assert.Equal(snapshot.RetrievedAt, result.RetrievedAt);
+ Assert.True(service.RefreshFailed);
+ Assert.Equal(1, result.OperationalCount);
+ await service.GetAsync(CancellationToken.None);
+ Assert.Equal(2, handler.Requests);
+ }
+
+ public void Dispose() { if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true); }
+ private class Factory(Handler handler) : IHttpClientFactory
+ { public HttpClient CreateClient(string name) => new(handler, disposeHandler: false); }
+ private class Handler : HttpMessageHandler
+ {
+ public int Requests;
+ public bool Unavailable;
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ Interlocked.Increment(ref Requests);
+ var json = request.RequestUri!.AbsoluteUri == StarlinkService.CatalogUrl
+ ? JsonSerializer.Serialize(new[] { Catalog(1, "+") }) : JsonSerializer.Serialize(new[] { Orbit(1) });
+ return Task.FromResult(new HttpResponseMessage(Unavailable ? HttpStatusCode.ServiceUnavailable : HttpStatusCode.OK)
+ { Content = new StringContent(json) });
+ }
+ }
+ private class EnvironmentStub : IWebHostEnvironment
+ {
+ public string ApplicationName { get; set; } = "Petabit";
+ public string EnvironmentName { get; set; } = "Testing";
+ public string ContentRootPath { get; set; } = "";
+ public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
+ public string WebRootPath { get; set; } = "";
+ public IFileProvider WebRootFileProvider { get; set; } = new NullFileProvider();
+ }
+}
diff --git a/Petabit.Tests/StationStatusTests.cs b/Petabit.Tests/StationStatusTests.cs
index abc779a..a720dac 100644
--- a/Petabit.Tests/StationStatusTests.cs
+++ b/Petabit.Tests/StationStatusTests.cs
@@ -15,12 +15,11 @@ public void StatusBecomesStaleAfterMaximumVerificationAge()
}
[Fact]
- [Trait("Category", "StationStatusFreshness")]
- public void CuratedStationStatusWasVerifiedRecently()
+ public void OfflineBootstrapDoesNotPretendToHaveCheckedNasaLive()
{
- Assert.False(
- StationStatus.IsStale(DateTimeOffset.UtcNow),
- $"Curated ISS status was last verified on {StationStatus.LastVerified:yyyy-MM-dd}. " +
- "Check the NASA source, update the data and move LastVerified forward.");
+ var state = StationSnapshot.Bootstrap();
+ Assert.Null(state.CheckedAt);
+ Assert.Equal(11, state.Crew.Length);
+ Assert.Contains(state.DockedVehicles, v => v.Name == "Crew-13 Dragon");
}
}
diff --git a/Petabit.Tests/StationSyncTests.cs b/Petabit.Tests/StationSyncTests.cs
new file mode 100644
index 0000000..2d3280b
--- /dev/null
+++ b/Petabit.Tests/StationSyncTests.cs
@@ -0,0 +1,139 @@
+using System.Net;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Petabit.Services;
+using Xunit;
+
+namespace Petabit.Tests;
+
+public class StationSyncTests : IDisposable
+{
+ private readonly string directory = Path.Combine(Path.GetTempPath(), "petabit-station-tests", Guid.NewGuid().ToString("N"));
+ private const string Overview = """
+ Oct. 1, 2026: International Space Station Configuration. Six spaceships are attached to the space station including Crew-12 and Crew-13 Dragons, Cygnus XL, Soyuz MS-29, and Progress 95 and 96.
+ """;
+ private const string Feed = """
+ -
+
Crew-12 Dragon Undocks from Station https://www.nasa.gov/blogs/spacestation/test/
+ Sat, 03 Oct 2026 19:19:36 +0000
+ The Crew-12 Dragon undocked from the International Space Station at 1:05 a.m. EDT.
]]>
+
+ """;
+
+ private StationStatusService Create(Handler handler, string? storage = null) => new(
+ new Factory(handler), NullLogger.Instance,
+ Options.Create(new StationSyncOptions { StoragePath = storage ?? Path.Combine(directory, "status.json") }),
+ new EnvironmentStub { ContentRootPath = directory });
+
+ [Fact]
+ public async Task ValidUpdateIsPersistedRestoredAndNotReplayed()
+ {
+ using var service = Create(new Handler(Feed, Overview));
+ await service.RefreshAsync(CancellationToken.None);
+ Assert.False(service.RefreshFailed);
+ Assert.False(service.PersistenceFailed);
+ Assert.Equal(7, service.Current.Crew.Length);
+ Assert.Equal(5, service.Current.DockedVehicles.Length);
+ Assert.True(File.Exists(Path.Combine(directory, "status.json")));
+ using var restarted = Create(new Handler(Feed, Overview));
+ await restarted.RestoreAsync(CancellationToken.None);
+ Assert.Equal(7, restarted.Current.Crew.Length);
+ await restarted.RefreshAsync(CancellationToken.None);
+ Assert.Single(restarted.Current.Events);
+ Assert.Equal(5, restarted.Current.DockedVehicles.Length); // Older diagram cannot re-add Crew-12.
+ }
+
+ [Theory]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ public async Task UnavailableOrChangedNasaPreservesPreviousState(bool unavailable, bool malformed)
+ {
+ var handler = new Handler(Feed, Overview);
+ using var service = Create(handler);
+ await service.RefreshAsync(CancellationToken.None);
+ var previous = service.Current;
+ handler.Unavailable = unavailable;
+ if (malformed) handler.Overview = "NASA changed its layout
";
+ await service.RefreshAsync(CancellationToken.None);
+ Assert.True(service.RefreshFailed);
+ Assert.Same(previous, service.Current);
+ }
+
+ [Fact]
+ public async Task InvalidSavedFileFallsBackToClearlyUnverifiedBootstrap()
+ {
+ Directory.CreateDirectory(directory);
+ await File.WriteAllTextAsync(Path.Combine(directory, "status.json"), "{broken");
+ using var service = Create(new Handler(Feed, Overview));
+ await service.RestoreAsync(CancellationToken.None);
+ Assert.Null(service.Current.CheckedAt);
+ Assert.Equal(11, service.Current.Crew.Length);
+ }
+
+ [Fact]
+ public async Task StorageFailureIsReportedWithoutDiscardingSuccessfulNasaUpdate()
+ {
+ Directory.CreateDirectory(directory);
+ var blocked = Path.Combine(directory, "blocked");
+ await File.WriteAllTextAsync(blocked, "file occupying directory path");
+ using var service = Create(new Handler(Feed, Overview), Path.Combine(blocked, "status.json"));
+ await service.RefreshAsync(CancellationToken.None);
+ Assert.True(service.PersistenceFailed);
+ Assert.False(service.RefreshFailed);
+ Assert.Equal(7, service.Current.Crew.Length);
+ }
+
+ [Fact]
+ public async Task ConditionalRequestsReuseUnchangedNasaDocuments()
+ {
+ var handler = new Handler(Feed, Overview) { Conditional = true };
+ using var service = Create(handler);
+ await service.RefreshAsync(CancellationToken.None);
+ var first = service.Current;
+ await service.RefreshAsync(CancellationToken.None);
+ Assert.False(service.RefreshFailed);
+ Assert.Equal(2, handler.NotModifiedCount);
+ Assert.Equal(first.Crew, service.Current.Crew);
+ Assert.Single(service.Current.Events);
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true);
+ }
+
+ private class Factory(Handler handler) : IHttpClientFactory
+ {
+ public HttpClient CreateClient(string name) => new(handler, disposeHandler: false);
+ }
+ private class Handler(string feed, string overview) : HttpMessageHandler
+ {
+ public bool Unavailable { get; set; }
+ public bool Conditional { get; set; }
+ public int NotModifiedCount;
+ public string Overview { get; set; } = overview;
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (Conditional && request.Headers.IfNoneMatch.Any())
+ {
+ Interlocked.Increment(ref NotModifiedCount);
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotModified));
+ }
+ var response = new HttpResponseMessage(Unavailable ? HttpStatusCode.ServiceUnavailable : HttpStatusCode.OK)
+ { Content = new StringContent(request.RequestUri!.AbsoluteUri == StationStatusService.FeedUrl ? feed : Overview) };
+ if (Conditional) response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue("\"station-test\"");
+ return Task.FromResult(response);
+ }
+ }
+ private class EnvironmentStub : IWebHostEnvironment
+ {
+ public string ApplicationName { get; set; } = "Petabit";
+ public string EnvironmentName { get; set; } = "Testing";
+ public string ContentRootPath { get; set; } = "";
+ public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
+ public string WebRootPath { get; set; } = "";
+ public IFileProvider WebRootFileProvider { get; set; } = new NullFileProvider();
+ }
+}
diff --git a/Petabit.Tests/iss-video-preview.test.cjs b/Petabit.Tests/iss-video-preview.test.cjs
new file mode 100644
index 0000000..0c40df8
--- /dev/null
+++ b/Petabit.Tests/iss-video-preview.test.cjs
@@ -0,0 +1,121 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const vm = require('node:vm');
+
+function harness() {
+ let now = 0;
+ let nextTimer = 0;
+ let requests = 0;
+ const timers = new Map();
+ const events = new Map();
+ const players = [];
+ const elements = new Map();
+ const strings = { videoLoading: 'loading', videoPlaying: 'playing', videoEnded: 'finished',
+ videoError: 'error', videoBlocked: 'blocked', videoTitle: 'ISS camera' };
+ for (const id of ['iss-video-panel', 'iss-led-localization', 'iss-video-stage', 'iss-video-status'])
+ elements.set(id, { hidden: true, dataset: id === 'iss-led-localization' ? strings : {},
+ textContent: '', children: [], replaceChildren(...children) { this.children = children; } });
+ const document = {
+ hidden: false,
+ getElementById: id => elements.get(id),
+ addEventListener: (type, callback) => events.set(type, callback),
+ createElement: () => ({}),
+ head: { append() { throw new Error('API should already be available in this fixture'); } }
+ };
+ const window = {
+ addEventListener: (type, callback) => events.set(type, callback),
+ setTimeout: (callback, delay) => { const id = ++nextTimer; timers.set(id, { callback, at: now + delay }); return id; },
+ clearTimeout: id => timers.delete(id),
+ YT: {
+ PlayerState: { PLAYING: 1, PAUSED: 2, BUFFERING: 3 },
+ Player: function (iframe, options) {
+ this.iframe = iframe;
+ this.options = options;
+ this.paused = 0;
+ this.destroyed = false;
+ this.mute = () => {};
+ this.playVideo = () => {};
+ this.pauseVideo = () => { this.paused++; };
+ this.destroy = () => { this.destroyed = true; };
+ this.state = state => options.events.onStateChange({ target: this, data: state });
+ players.push(this);
+ }
+ }
+ };
+ const context = vm.createContext({ window, document, location: { origin: 'https://petabit.example' },
+ performance: { now: () => now }, AbortSignal,
+ fetch: async () => { requests++; return { ok: true, json: async () => ({ videoId: 'awQzjn72bI0' }) }; } });
+ vm.runInContext(fs.readFileSync(path.join(__dirname, '../Petabit/wwwroot/js/iss-video-preview.js'), 'utf8'), context);
+ events.get('DOMContentLoaded')();
+ return {
+ elements, players, document, events, requests: () => requests,
+ start: () => events.get('iss-ping')(),
+ tick(ms) {
+ const until = now + ms;
+ while (true) {
+ const next = [...timers].filter(([, timer]) => timer.at <= until).sort((a, b) => a[1].at - b[1].at)[0];
+ if (!next) break;
+ timers.delete(next[0]); now = next[1].at; next[1].callback();
+ }
+ now = until;
+ }
+ };
+}
+
+test('does not contact YouTube/NASA or show the panel before manual Ping', () => {
+ const app = harness();
+ assert.equal(app.requests(), 0);
+ assert.equal(app.elements.get('iss-video-panel').hidden, true);
+ assert.equal(app.players.length, 0);
+});
+
+test('counts five seconds of playback, excluding loading and buffering', async () => {
+ const app = harness();
+ await app.start();
+ const player = app.players[0];
+ app.tick(4000); // Initial connection is not part of the preview.
+ assert.equal(player.paused, 0);
+ player.state(1);
+ app.tick(2000);
+ player.state(3);
+ app.tick(3000); // Buffering is not part of the preview either.
+ player.state(1);
+ app.tick(2999);
+ assert.equal(player.paused, 0);
+ app.tick(1);
+ assert.equal(player.paused, 1);
+ assert.equal(app.elements.get('iss-video-panel').dataset.playback, 'finished');
+});
+
+test('a repeated Ping discards the previous timer and starts a new live preview', async () => {
+ const app = harness();
+ await app.start();
+ const first = app.players[0];
+ first.state(1);
+ app.tick(4000);
+ await app.start();
+ assert.equal(first.destroyed, true);
+ const second = app.players[1];
+ second.state(1);
+ app.tick(1000);
+ assert.equal(second.paused, 0);
+ app.tick(4000);
+ assert.equal(second.paused, 1);
+});
+
+test('tab hiding pauses playback and does not consume the remaining preview', async () => {
+ const app = harness();
+ await app.start();
+ const player = app.players[0];
+ player.state(1);
+ app.tick(1000);
+ app.document.hidden = true;
+ app.events.get('visibilitychange')();
+ app.tick(10000);
+ assert.equal(player.paused, 1);
+ player.state(1);
+ app.tick(4000);
+ assert.equal(player.paused, 2);
+});
diff --git a/Petabit.Tests/starlink-orbits.test.mjs b/Petabit.Tests/starlink-orbits.test.mjs
new file mode 100644
index 0000000..9802b12
--- /dev/null
+++ b/Petabit.Tests/starlink-orbits.test.mjs
@@ -0,0 +1,31 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { createRecords, positionsAt } from '../Petabit/wwwroot/js/starlink-orbits.mjs';
+const omm = { OBJECT_NAME: 'STARLINK-1008', OBJECT_ID: '2019-074B', NORAD_CAT_ID: 44714,
+ EPOCH: '2026-10-04T22:36:12.73824', MEAN_MOTION: 15.66040205, ECCENTRICITY: .0002923,
+ INCLINATION: 53.1448, RA_OF_ASC_NODE: 256.2279, ARG_OF_PERICENTER: 92.7295,
+ MEAN_ANOMALY: 267.4056, EPHEMERIS_TYPE: 0, CLASSIFICATION_TYPE: 'U',
+ ELEMENT_SET_NO: 999, REV_AT_EPOCH: 38133, BSTAR: .00047264149,
+ MEAN_MOTION_DOT: .00049034, MEAN_MOTION_DDOT: 0 };
+test('OMM and six-digit catalog IDs give a valid Earth-fixed orbital position', () => {
+ const records = createRecords([{ ...omm, NORAD_CAT_ID: 100800 }]);
+ const frame = positionsAt(records, Date.parse(omm.EPOCH + 'Z'));
+ assert.equal(frame.count, 1);
+ assert.ok([...frame.packed].every(Number.isFinite));
+ const height = Math.hypot(...frame.packed.slice(0, 3)) - 6371;
+ assert.ok(height > 300 && height < 700);
+});
+test('real-time positions move and Earth-fixed velocity predicts the next second', () => {
+ const records = createRecords([omm]);
+ const now = Date.parse(omm.EPOCH + 'Z');
+ const a = positionsAt(records, now).packed;
+ const b = positionsAt(records, now + 1000).packed;
+ const displacement = Math.hypot(...[0, 1, 2].map(i => b[i] - a[i]));
+ assert.ok(displacement > 6 && displacement < 9);
+ const error = Math.hypot(...[0, 1, 2].map(i => b[i] - (a[i] + a[i + 3])));
+ assert.ok(error < .02, `Interpolation error: ${error} km`);
+});
+test('unpropagatable elements are omitted rather than displaying invented locations', () => {
+ const records = createRecords([{ ...omm, ECCENTRICITY: 2 }]);
+ assert.equal(positionsAt(records, Date.parse(omm.EPOCH + 'Z')).count, 0);
+});
diff --git a/Petabit/Controllers/HomeController.cs b/Petabit/Controllers/HomeController.cs
index a50ebe1..97d1b16 100644
--- a/Petabit/Controllers/HomeController.cs
+++ b/Petabit/Controllers/HomeController.cs
@@ -14,11 +14,14 @@ public class HomeController : Controller
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly Petabit.Services.StationStatusService _station;
- public HomeController(ILogger logger, IHttpClientFactory httpClientFactory)
+ public HomeController(ILogger logger, IHttpClientFactory httpClientFactory,
+ Petabit.Services.StationStatusService station)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
+ _station = station;
}
[HttpPost]
@@ -63,6 +66,73 @@ public IActionResult SetLanguage(string culture, string returnUrl = "/")
[HttpGet]
public IActionResult ISSTracker() => View();
+ [HttpGet]
+ [EnableRateLimiting("iss")]
+ [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+ public IActionResult StationData() => Json(StationPayload());
+
+ [HttpGet]
+ [EnableRateLimiting("iss")]
+ public async Task VideoSource([FromServices] Petabit.Services.NasaLiveVideoService video,
+ CancellationToken cancellationToken)
+ {
+ try { return Json(await video.GetAsync(cancellationToken)); }
+ catch (Exception error) when (error is HttpRequestException or OperationCanceledException or FormatException
+ or System.Text.RegularExpressions.RegexMatchTimeoutException)
+ {
+ _logger.LogWarning(error, "NASA live video source is temporarily unavailable.");
+ return Problem("NASA live video is temporarily unavailable.", statusCode: StatusCodes.Status503ServiceUnavailable);
+ }
+ }
+
+ private object StationPayload()
+ {
+ var state = _station.Current;
+ return new
+ {
+ astronautCount = state.Crew.Length,
+ astronauts = state.Crew,
+ dockedVehicles = state.DockedVehicles,
+ crewUpdatedAt = state.CrewUpdatedAt,
+ vehiclesUpdatedAt = state.VehiclesUpdatedAt,
+ stationStatusUpdatedAt = state.CrewUpdatedAt < state.VehiclesUpdatedAt ? state.CrewUpdatedAt : state.VehiclesUpdatedAt,
+ stationStatusCheckedAt = state.CheckedAt,
+ stationStatusSource = state.VehicleSource,
+ crewSource = state.CrewSource,
+ stationStatusIsStale = state.CheckedAt is null || DateTimeOffset.UtcNow - state.CheckedAt > TimeSpan.FromMinutes(10),
+ stationStatusNeedsReview = state.NeedsReview,
+ stationStatusRefreshFailed = _station.RefreshFailed,
+ stationStatusPersistenceFailed = _station.PersistenceFailed,
+ events = state.Events
+ };
+ }
+
+ [HttpGet]
+ [EnableRateLimiting("iss")]
+ [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+ public async Task StarlinkData([FromServices] Petabit.Services.StarlinkService starlink,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ var data = await starlink.GetAsync(cancellationToken);
+ return Json(new
+ {
+ data.OperationalCount, data.OnOrbitCount, data.PartiallyOperationalCount, data.RetrievedAt,
+ data.OldestEpoch, data.NewestEpoch, data.Elements,
+ isStale = DateTimeOffset.UtcNow - data.RetrievedAt > TimeSpan.FromHours(4)
+ || DateTimeOffset.UtcNow - data.OldestEpoch > TimeSpan.FromDays(3.5),
+ starlink.RefreshFailed, starlink.PersistenceFailed,
+ sourceUrl = "https://celestrak.org/satcat/status.php"
+ });
+ }
+ catch (Exception error) when (error is HttpRequestException or OperationCanceledException)
+ {
+ _logger.LogWarning(error, "Starlink data is temporarily unavailable.");
+ return Problem("Starlink data is temporarily unavailable.", statusCode: StatusCodes.Status503ServiceUnavailable);
+ }
+ }
+
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error() => View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
@@ -83,17 +153,19 @@ public async Task Data(CancellationToken cancellationToken)
return Problem("ISS service returned no data.", statusCode: StatusCodes.Status503ServiceUnavailable);
}
+ var state = _station.Current;
return Json(new
{
latitude = iss.Latitude,
longitude = iss.Longitude,
speed = iss.Velocity,
- astronautCount = StationStatus.Crew.Count,
- astronauts = StationStatus.Crew,
- dockedVehicles = StationStatus.DockedVehicles,
- stationStatusUpdatedAt = StationStatus.LastVerified,
- stationStatusSource = StationStatus.SourceUrl,
- stationStatusIsStale = StationStatus.IsStale(DateTimeOffset.UtcNow)
+ positionUpdatedAt = DateTimeOffset.UtcNow,
+ astronautCount = state.Crew.Length,
+ astronauts = state.Crew,
+ dockedVehicles = state.DockedVehicles,
+ stationStatusUpdatedAt = state.CrewUpdatedAt < state.VehiclesUpdatedAt ? state.CrewUpdatedAt : state.VehiclesUpdatedAt,
+ stationStatusSource = state.VehicleSource,
+ stationStatusIsStale = state.CheckedAt is null || DateTimeOffset.UtcNow - state.CheckedAt > TimeSpan.FromMinutes(10)
});
}
catch (HttpRequestException exception)
diff --git a/Petabit/Dockerfile b/Petabit/Dockerfile
index b1e4f47..e2f7a37 100644
--- a/Petabit/Dockerfile
+++ b/Petabit/Dockerfile
@@ -11,6 +11,10 @@ FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
+COPY Petabit/entrypoint.sh /app/entrypoint.sh
+
+RUN mkdir -p /app/App_Data && chown "$APP_UID" /app/App_Data \
+ && sed -i 's/\r$//' /app/entrypoint.sh && chmod 755 /app/entrypoint.sh
USER $APP_UID
-ENTRYPOINT ["sh", "-c", "exec dotnet Petabit.dll --urls http://0.0.0.0:${PORT:-3000}"]
+ENTRYPOINT ["/app/entrypoint.sh"]
diff --git a/Petabit/Models/StationSnapshot.cs b/Petabit/Models/StationSnapshot.cs
new file mode 100644
index 0000000..286f613
--- /dev/null
+++ b/Petabit/Models/StationSnapshot.cs
@@ -0,0 +1,22 @@
+namespace Petabit.Models;
+
+public record StationEvent(string Id, string Title, string SourceUrl, DateTimeOffset PublishedAt, string Kind);
+
+public record StationSnapshot(
+ CrewMember[] Crew,
+ DockedVehicle[] DockedVehicles,
+ DateTimeOffset CrewUpdatedAt,
+ DateTimeOffset VehiclesUpdatedAt,
+ DateTimeOffset? CheckedAt,
+ string CrewSource,
+ string VehicleSource,
+ StationEvent[] Events,
+ string[] ProcessedIds,
+ bool NeedsReview = false)
+{
+ public static StationSnapshot Bootstrap() => new(
+ StationStatus.Crew.ToArray(), StationStatus.DockedVehicles.ToArray(),
+ StationStatus.LastVerified, new(2026, 10, 1, 0, 0, 0, TimeSpan.Zero), null,
+ "https://www.nasa.gov/blogs/spacestation/2026/10/02/station-crew-expands-as-nasas-spacex-crew-12-prepares-to-depart/",
+ StationStatus.SourceUrl, [], []);
+}
diff --git a/Petabit/Models/StationStatus.cs b/Petabit/Models/StationStatus.cs
index 148ba29..4164346 100644
--- a/Petabit/Models/StationStatus.cs
+++ b/Petabit/Models/StationStatus.cs
@@ -1,6 +1,6 @@
namespace Petabit.Models;
-public record CrewMember(string Name, string Country, string Agency);
+public record CrewMember(string Name, string Country, string Agency, string Mission = "");
public record DockedVehicle(string Name, string Purpose, string Operator);
@@ -8,29 +8,35 @@ public static class StationStatus
{
public static readonly TimeSpan MaximumVerificationAge = TimeSpan.FromDays(45);
- // NASA's public station-status pages were checked on 14 September 2026.
- // Keep this curated data separate from the live orbital-position feed.
+ // Offline bootstrap, confirmed by NASA's 2 October station report.
+ // The background synchronizer replaces this with persisted NASA observations.
public static readonly IReadOnlyList Crew =
[
- new("Jessica Meir", "SAD", "NASA"),
- new("Jack Hathaway", "SAD", "NASA"),
- new("Sophie Adenot", "Francuska", "ESA"),
- new("Andrey Fedyaev", "Rusija", "Roscosmos"),
- new("Anil Menon", "SAD", "NASA"),
- new("Pyotr Dubrov", "Rusija", "Roscosmos"),
- new("Anna Kikina", "Rusija", "Roscosmos")
+ new("Jessica Meir", "SAD", "NASA", "Crew-12"),
+ new("Jack Hathaway", "SAD", "NASA", "Crew-12"),
+ new("Sophie Adenot", "Francuska", "ESA", "Crew-12"),
+ new("Andrey Fedyaev", "Rusija", "Roscosmos", "Crew-12"),
+ new("Anil Menon", "SAD", "NASA", "Soyuz MS-29"),
+ new("Pyotr Dubrov", "Rusija", "Roscosmos", "Soyuz MS-29"),
+ new("Anna Kikina", "Rusija", "Roscosmos", "Soyuz MS-29"),
+ new("Jessica Watkins", "SAD", "NASA", "Crew-13"),
+ new("Luke Delaney", "SAD", "NASA", "Crew-13"),
+ new("Joshua Kutryk", "Kanada", "CSA", "Crew-13"),
+ new("Sergey Teteryatnikov", "Rusija", "Roscosmos", "Crew-13")
];
public static readonly IReadOnlyList DockedVehicles =
[
new("Crew-12 Dragon", "Posadna letjelica", "SpaceX / NASA"),
+ new("Crew-13 Dragon", "Posadna letjelica", "SpaceX / NASA"),
new("Cygnus XL", "Teretna letjelica", "Northrop Grumman / NASA"),
new("Soyuz MS-29", "Posadna letjelica", "Roscosmos"),
- new("Progress 95", "Teretna letjelica", "Roscosmos")
+ new("Progress 95", "Teretna letjelica", "Roscosmos"),
+ new("Progress 96", "Teretna letjelica", "Roscosmos")
];
- public static readonly DateTimeOffset LastVerified = new(2026, 9, 14, 0, 0, 0, TimeSpan.Zero);
- public const string SourceUrl = "https://www.nasa.gov/international-space-station/space-station-visiting-vehicles/";
+ public static readonly DateTimeOffset LastVerified = new(2026, 10, 2, 18, 14, 13, TimeSpan.Zero);
+ public const string SourceUrl = "https://www.nasa.gov/international-space-station/space-station-overview/";
public static bool IsStale(DateTimeOffset now) => now - LastVerified > MaximumVerificationAge;
}
diff --git a/Petabit/Petabit.csproj b/Petabit/Petabit.csproj
index e5e2032..ddbc696 100644
--- a/Petabit/Petabit.csproj
+++ b/Petabit/Petabit.csproj
@@ -11,6 +11,8 @@
+
+
diff --git a/Petabit/Program.cs b/Petabit/Program.cs
index 2853441..21b00c4 100644
--- a/Petabit/Program.cs
+++ b/Petabit/Program.cs
@@ -122,6 +122,23 @@ public static void Main(string[] args)
});
builder.Services.AddHealthChecks()
.AddCheck("iss-api", tags: ["ready"]);
+ builder.Services.Configure(builder.Configuration.GetSection("StationSync"));
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddHttpClient("celestrak", client =>
+ {
+ client.Timeout = TimeSpan.FromSeconds(25);
+ client.MaxResponseContentBufferSize = 12_000_000;
+ client.DefaultRequestHeaders.UserAgent.ParseAdd("Petabit/1.0 (+https://petabit-production.up.railway.app)");
+ });
+ builder.Services.AddHostedService(services => services.GetRequiredService());
+ builder.Services.AddHttpClient("nasa", client =>
+ {
+ client.Timeout = TimeSpan.FromSeconds(15);
+ client.MaxResponseContentBufferSize = 2_000_000;
+ client.DefaultRequestHeaders.UserAgent.ParseAdd("Petabit/1.0 (+https://petabit-production.up.railway.app)");
+ });
builder.Services.AddOutputCache();
builder.Services.AddRateLimiter(options =>
{
@@ -197,7 +214,8 @@ public static void Main(string[] args)
"form-action 'self'; " +
"frame-ancestors 'none'; " +
"object-src 'none'; " +
- $"script-src 'self' 'nonce-{cspNonce}' https://www.googletagmanager.com; " +
+ $"script-src 'self' 'nonce-{cspNonce}' https://www.googletagmanager.com https://www.youtube.com; " +
+ "frame-src https://www.youtube-nocookie.com; " +
$"style-src 'self' 'nonce-{cspNonce}'; " +
"img-src 'self' data:; " +
"font-src 'self' data:; " +
diff --git a/Petabit/Resources/Views.Home.ISSTracker.de.resx b/Petabit/Resources/Views.Home.ISSTracker.de.resx
index db0b3bf..9f01f18 100644
--- a/Petabit/Resources/Views.Home.ISSTracker.de.resx
+++ b/Petabit/Resources/Views.Home.ISSTracker.de.resx
@@ -5,4 +5,5 @@
Daten werden abgerufen... Position: {0}, {1} Geschwindigkeit: {0} km/h Personen an Bord der ISS: {0} Besatzung An der ISS angedockt Referenzstatus: {0}, Daten konnten nicht abgerufen werden.
USA Frankreich Russland Bemanntes Raumfahrzeug Frachtraumfahrzeug
Die Angaben zu Besatzung und angedockten Raumfahrzeugen könnten veraltet sein.
+Kanada
diff --git a/Petabit/Resources/Views.Home.ISSTracker.en.resx b/Petabit/Resources/Views.Home.ISSTracker.en.resx
index 0168394..f62c5f2 100644
--- a/Petabit/Resources/Views.Home.ISSTracker.en.resx
+++ b/Petabit/Resources/Views.Home.ISSTracker.en.resx
@@ -5,4 +5,5 @@
Retrieving data... Location: {0}, {1} Speed: {0} km/h Total aboard the ISS: {0} Crew Docked at the ISS Reference status: {0}, Unable to retrieve data.
USA France Russia Crew spacecraft Cargo spacecraft
Crew and docked-vehicle details may be outdated.
+Canada
diff --git a/Petabit/Resources/Views.Home.ISSTracker.hr.resx b/Petabit/Resources/Views.Home.ISSTracker.hr.resx
index bba7170..f403c2e 100644
--- a/Petabit/Resources/Views.Home.ISSTracker.hr.resx
+++ b/Petabit/Resources/Views.Home.ISSTracker.hr.resx
@@ -5,4 +5,5 @@
Dohvaćam podatke... Lokacija: {0}, {1} Brzina: {0} km/h Ukupno na ISS-u: {0} Posada Spojeno na ISS Referentno stanje: {0}, Greška prilikom dohvaćanja podataka.
SAD Francuska Rusija Posadna letjelica Teretna letjelica
Podaci o posadi i spojenim letjelicama možda su zastarjeli.
+Kanada
diff --git a/Petabit/Resources/Views.Home.Index.de.resx b/Petabit/Resources/Views.Home.Index.de.resx
index 0a578ea..0ca71f6 100644
--- a/Petabit/Resources/Views.Home.Index.de.resx
+++ b/Petabit/Resources/Views.Home.Index.de.resx
@@ -1,4 +1,4 @@
-
+
Petabit 🌍
@Localizer["WelcomeText"]
-
-
-
🌐 ISS Tracker
-
📡🛰️ Ping ISS
+
@Localizer["Refresh"] @Localizer["Sound"]
+
🛰 Ping Starlink
+
+
+
+
+
@@ -74,7 +103,42 @@
-
+
+
+
+
+
@Localizer["LedLocation"] — @Localizer["Loading"]
+
@Localizer["LedSpeed"] — km/h
+
@Localizer["LedCrew"] — @Localizer["OnStation"]
+
@Localizer["VehiclesTitle"] — @Localizer["DockedStatus"]
+
+
@Localizer["Loading"]
+
+
+
+
@Localizer["VehiclesTitle"]
+
+
@Localizer["EventsTitle"] @Localizer["EventsNote"]
+
@@ -83,34 +147,18 @@
}
@section Scripts {
+
+
+
+ window.addEventListener('iss-position-unavailable', restartGlobe);
+ document.addEventListener('visibilitychange', () => {
+ if (document.hidden) cancelAnimationFrame(globeAnimationFrame);
+ else restartGlobe();
+ });
}
diff --git a/Petabit/appsettings.json b/Petabit/appsettings.json
index 6f44a3b..260959f 100644
--- a/Petabit/appsettings.json
+++ b/Petabit/appsettings.json
@@ -16,5 +16,10 @@
"ForwardedHeaders": {
"KnownProxies": [],
"KnownNetworks": []
+ },
+ "StationSync": {
+ "StoragePath": "App_Data/station-status.json",
+ "PollSeconds": 120,
+ "Enabled": true
}
-}
+}
\ No newline at end of file
diff --git a/Petabit/docs/nasa-station-sync.md b/Petabit/docs/nasa-station-sync.md
new file mode 100644
index 0000000..f9342b0
--- /dev/null
+++ b/Petabit/docs/nasa-station-sync.md
@@ -0,0 +1,50 @@
+# NASA station synchronization
+
+The application checks NASA's station blog RSS and the dated configuration caption on the Station Overview page on startup and every 120 seconds. It does not use the potentially outdated `TODAY` counters. The browser reads station status and orbital position independently every 30 seconds; no browser needs to be open for NASA synchronization to run.
+
+## Configuration and Railway storage
+
+`StationSync:Enabled` defaults to `true`. `StationSync:PollSeconds` defaults to 120 and is bounded to 60–3600 seconds. `StationSync:StoragePath` defaults to `App_Data/station-status.json`, relative to the application's content root. Environment variables use double underscores, e.g. `StationSync__PollSeconds=120`.
+
+The Docker image creates `/app/App_Data` with write access for its non-root runtime user. Configure a **Railway persistent volume mounted at `/app/App_Data`** to preserve snapshots between deployments/container replacements. Without that volume, the local container file can be lost when Railway replaces the container. The environment variable `StationSync__StoragePath` can point to a different writable persistent mount. Run a single application replica with this file store; multiple replicas need a shared database and a single synchronization worker.
+
+Railway mounts volumes as root. Set `RAILWAY_RUN_UID=0` so the entrypoint can assign `/app/App_Data` to the image's app user; the entrypoint then drops privileges using `runuser` before starting .NET. The web process remains unprivileged. Without Railway's UID override, the default image runs directly as the app user.
+
+NASA data is written to a temporary file and then atomically replaces the saved snapshot. A successful network check with failed disk storage remains available in memory and emits a persistence warning. Do not put the snapshot under `wwwroot`. `App_Data` is excluded from source control. The initial offline snapshot was manually confirmed from NASA's October 1 configuration and October 2 crew report; it is clearly marked unverified until a live check succeeds.
+
+## Rules and limits
+
+- Completed docking updates spacecraft. Hatch entry updates crew separately.
+- Completed undocking removes the identified vehicle and the crew assigned to that mission.
+- Future plans, launch coverage, and departure announcements remain news and do not change station state.
+- NASA headlines are kept in English with a direct source link. Displayed event times are publication times in the browser's timezone, not inferred physical event times. Date-only configuration observations are displayed as dates.
+- Supported vehicle identifiers currently include Crew-N Dragon, CRS-N Dragon, Soyuz MS-N, Progress N / MS-N, and Cygnus / Cygnus XL. New families and ambiguous wording require review. A Cygnus without a numbered identifier represents the single named Cygnus in the dated configuration.
+- Crew entry requires agency-prefixed names. ESA does not identify a nationality: it remains blank unless already known from the verified roster. No nationality is guessed.
+- An incomplete configuration, malformed XML, unsupported event or missing RSS continuity preserves the last valid data. A successful HTTP response only confirms the feed was checked; it does not mean the crew changed recently.
+- A date-only diagram cannot undo a newer timestamped event. A changed diagram without a confirmed crew departure flags review rather than inventing a crew change.
+- RSS covers a finite recent window. Extended downtime may require manual reconciliation. `NeedsReview` stays set until an operator has checked and reconciled the saved roster.
+- HTTP conditional requests use ETag/Last-Modified where NASA provides them. Polling latency is normally up to 120 seconds plus the browser's next 30-second refresh, **after NASA publishes the confirmation**.
+
+## Review and recovery
+
+`/Home/StationData` exposes the crew, vehicles, source links, individual confirmation dates, `stationStatusCheckedAt`, and stale/review/refresh/persistence flags. It remains available when the independent position provider is down. This endpoint never triggers a NASA download.
+
+For a review: inspect the NASA source and runtime logs, stop the worker/application, back up the saved JSON, reconcile the affected crew/vehicles and their source/confirmation dates, and only then clear `NeedsReview`. Restart the application and confirm `/Home/StationData` reports a recent check. Do not clear review merely to hide the warning. If NASA changes its wording or HTML structure, update the parser with a regression fixture before resuming automatic interpretation.
+
+The hourly GitHub workflow now checks production synchronization instead of expiring the static offline bootstrap. It requires the updated application to be deployed. It fails for stale data, pending review, refresh errors or failed persistence.
+
+## Validation
+
+Parser tests cover dated configurations, shared vehicle numbering, unknown vehicles, forecasts, docking versus crew entry, mission-specific undocking, agency/name extraction, XML safety and source URLs. Synchronizer tests cover persistence/restart/deduplication, upstream failures, changed HTML, invalid saved JSON and unwritable storage. Integration tests check the station endpoint when the position API fails. Tests use fixed NASA fixtures without making network requests.
+
+## Manual Ping: LED and live camera
+
+The green Matrix header is retained. The LED report and video panel appear only after a manual `Ping ISS`; scheduled data refreshes update existing reports without reopening video. The LED includes position, speed, crew, vehicles and NASA freshness. The requested horizontal ticker stays animated, with speed adjusted to the text length. The compact video panel floats in the bottom-right corner.
+
+`/Home/VideoSource` reads the camera embed from NASA's official `https://eol.jsc.nasa.gov/ESRS/HDEV/` page, caches the identifier for 15 minutes, and rejects unexpected hosts or malformed IDs. YouTube's API and privacy-enhanced iframe load only after manual Ping. The player is muted and pauses after five seconds of actual playback; connection and buffering time are excluded. A new Ping recreates the player for a new live view. If autoplay is blocked, the user can press Play inside the player. Source/player failures show a message and a link to the NASA source. This is a live preview, not a downloaded recording.
+
+Browser media timing tests run without network dependencies:
+
+```powershell
+node --test Petabit.Tests/iss-video-preview.test.cjs
+```
diff --git a/Petabit/docs/starlink-tracking.md b/Petabit/docs/starlink-tracking.md
new file mode 100644
index 0000000..ee11229
--- /dev/null
+++ b/Petabit/docs/starlink-tracking.md
@@ -0,0 +1,31 @@
+# Starlink tracking
+
+`Ping Starlink` opens an independent panel; neither the panel nor its data is loaded on initial page load. `Ping ISS` alone opens the LED ticker and live video. The ticker scrolls horizontally with a duration based on text length.
+
+## Data and counts
+
+- Orbital elements: https://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=JSON
+- Catalog: https://celestrak.org/satcat/records.php?NAME=STARLINK&FORMAT=JSON
+- Operational status definitions: https://celestrak.org/satcat/status.php
+
+Only Starlink payloads with Earth as their orbit center, `ORB` orbit type and no decay date count as in orbit. **Operational** is strictly `OPS_STATUS_CODE = +`. `P` is shown separately. Active (`+, P, B, S, X`) is broader than operational and is not substituted for it. Catalog status is an external classification, not a guarantee a satellite is currently serving internet customers.
+
+The operational count includes cataloged satellites without available GP data. The plotted count explicitly reports how many of those have usable orbital elements. Failed propagation is omitted, never replaced with synthetic locations. Objects without cataloged identities are outside this count.
+
+## Position calculation
+
+Satellite.js 7.1.0 (MIT, vendored in `wwwroot/lib/satellite-js`) consumes JSON OMM elements, including six-digit IDs, and uses SGP4. A module worker propagates all matching operational satellites every second. Earth-fixed position/velocity interpolation produces smooth motion at real-time speed (1×). Earth occludes satellites behind it. Drag the globe horizontally to change the view; the motion checkbox freezes/resumes positions. Hidden tabs stop propagation and animation.
+
+These are predicted positions from published elements, not direct satellite telemetry. Retrieval time, oldest/newest element epochs, and a stale-data warning are shown. Data is stale when retrieval exceeds four hours or the oldest element exceeds 3.5 days.
+
+## Caching and deployment
+
+All users share one singleton cache. CelesTrak GP updates at two-hour intervals and enforces one download per update; repeated pings reuse the cached data. The snapshot is atomically persisted to `App_Data/starlink-status.json` and restored on restart. An unsuccessful refresh preserves the last validated snapshot and waits two hours before another attempt, also protecting against re-downloading one successful half of a failed two-source refresh. With no saved snapshot the endpoint returns 503 rather than an invented count. There is no upstream request before the first Starlink ping.
+
+Production should use one app replica and a persistent `/app/App_Data` volume, as with NASA station synchronization. Multiple independent replicas require a shared cache/refresh coordinator. Local cached data and test downloads are excluded from publish and Git.
+
+## Validation
+
+`dotnet test Petabit.sln --configuration Release` covers catalog classification, invalid data, six-digit IDs, deduplication, persisted cache/restart and failure retention. `node --test Petabit.Tests/*.test.cjs Petabit.Tests/*.test.mjs` covers real-time propagation, Earth-fixed velocity interpolation and invalid-orbit omission, plus ISS preview playback.
+
+Vendored npm tarball SHA-256: `B812DA8AF116EA123F9D3763CE2FBCF7A0559C6DD1AF25B0AC9DA567CCF2A1B5`. No browser CDN or WebAssembly dependency is needed.
diff --git a/Petabit/entrypoint.sh b/Petabit/entrypoint.sh
new file mode 100644
index 0000000..db17fa9
--- /dev/null
+++ b/Petabit/entrypoint.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+
+# Railway mounts persistent volumes as root. Initialize only this application's
+# data directory, then run the web process as the image's unprivileged app user.
+if [ "$(id -u)" = "0" ]; then
+ mkdir -p /app/App_Data
+ chown -R "$APP_UID:$APP_UID" /app/App_Data
+ exec runuser -u app -- sh -c 'exec dotnet Petabit.dll --urls "http://0.0.0.0:${PORT:-3000}"'
+fi
+
+exec dotnet Petabit.dll --urls "http://0.0.0.0:${PORT:-3000}"
diff --git a/Petabit/wwwroot/css/iss-hologram.css b/Petabit/wwwroot/css/iss-hologram.css
index a01c349..0eafed0 100644
--- a/Petabit/wwwroot/css/iss-hologram.css
+++ b/Petabit/wwwroot/css/iss-hologram.css
@@ -1,366 +1,127 @@
-body {
- margin-bottom: 0;
-}
-
.home-hero {
- position: relative;
- isolation: isolate;
- min-height: calc(75vh - 1rem);
- margin-top: 0 !important;
- padding-block: 0.65rem;
-}
-
-.home-hero > :not(.iss-hologram) {
- position: relative;
- z-index: 2;
-}
-
-.iss-hologram {
- position: absolute;
- inset: 0;
- z-index: 0;
- width: 100%;
- height: 75vh;
- margin: 0;
- padding: 0;
- overflow: hidden;
- pointer-events: none;
- opacity: 0.48;
- -webkit-mask-image: radial-gradient(ellipse at center, #000 36%, transparent 76%);
- mask-image: radial-gradient(ellipse at center, #000 36%, transparent 76%);
- transition: opacity 0.5s ease, filter 0.5s ease;
-}
-
-.iss-hologram.is-tracking {
- z-index: 4;
- opacity: 1;
- overflow: visible;
- -webkit-mask-image: none;
- mask-image: none;
- filter: saturate(1.3) brightness(1.16) drop-shadow(0 0 24px rgba(98, 255, 225, 0.34));
- animation: hologramReveal 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
-}
-
-.iss-hologram.is-tracking .iss-hologram-stage {
- overflow: visible;
- transform: translateY(-5%) scale(2.32);
-}
-
-.iss-hologram-heading {
- display: none;
- align-items: center;
- justify-content: space-between;
- gap: 1rem;
- text-align: left;
-}
-
-.iss-hologram-heading h3 {
- margin: 0;
-}
-
-.iss-hologram-kicker {
- margin: 0 0 0.2rem;
- color: #62ffe1;
- font: 600 0.72rem/1.2 monospace;
- letter-spacing: 0.18em;
-}
-
-.iss-hologram-status {
- flex: 0 0 auto;
- padding: 0.35rem 0.65rem;
- border: 1px solid rgba(98, 255, 225, 0.24);
- border-radius: 999px;
- color: #b8fff1;
- font: 0.74rem/1.2 monospace;
-}
-
-.iss-hologram-status.is-live::before {
- content: '';
- display: inline-block;
- width: 0.45rem;
- height: 0.45rem;
- margin-right: 0.4rem;
- border-radius: 50%;
- background: #62ffe1;
- box-shadow: 0 0 10px #62ffe1;
-}
-
-.iss-hologram-stage {
- position: relative;
- width: 100%;
- height: 100%;
- margin: 0;
- overflow: hidden;
- transform: translateY(-9%) scale(2.15);
- transform-origin: center;
-}
-
-#iss-globe-canvas {
- display: block;
- width: 100%;
- height: 100%;
-}
-
-.iss-scanline {
- position: absolute;
- inset: -20% 0 auto;
- height: 26%;
- pointer-events: none;
- background: linear-gradient(to bottom, transparent, rgba(100, 255, 226, 0.07), transparent);
- animation: issScan 6s linear infinite;
-}
-
-.iss-hologram-note {
- display: none;
- margin: 0;
- color: rgba(220, 255, 248, 0.64);
- font-size: 0.75rem;
-}
-
-.home-hero > h1 {
- margin-bottom: 0.25rem;
- font-size: clamp(2rem, 5vh, 3.2rem);
-}
-
-.home-hero > .lead {
- margin-bottom: 0.45rem;
-}
-
+ --station-bg: #101f27; --station-border: #29424b; --station-accent: #72e5ce; --station-muted: #aec0c8;
+ max-width: 1120px; margin-top: 0 !important; padding: 2rem 1rem 3rem; text-align: left !important;
+}
+body .home-hero :is(h1, h2, h3, .btn) { text-shadow: none !important; }
+body.dark-mode .home-hero a.btn.btn-home, body.light-mode .home-hero a.btn.btn-home { box-shadow: none !important; }
+body.light-mode:has(.home-hero) { background: #edf4f6; color: #1d3038; }
+.home-hero > h1 { font-size: clamp(2rem, 5vw, 3rem); font-weight: 700; letter-spacing: -.04em; }
+.home-hero > .lead { color: var(--station-muted); max-width: 42rem; }
+.home-hero .row { justify-content: flex-start !important; row-gap: .5rem; }
+.home-hero .btn-home { box-shadow: none; font-size: .9rem; padding: .5rem 1rem; }
+.home-hero h2 { text-align: left !important; margin: 2rem 0 1rem; font-weight: 700; }
+.home-hero #iss-ping-button { background: #72e5ce; color: #092029; border: 0; box-shadow: none; text-shadow: none; min-height: 44px; padding: .65rem 1.2rem; }
+.iss-sound-option { display: inline-flex; align-items: center; gap: .5rem; margin-left: 1rem; color: var(--station-muted); font-size: .9rem; min-height: 44px; }
+.iss-hologram { position: relative; margin: 1.5rem 0 0; padding: 1.25rem; border: 1px solid var(--station-border); border-radius: 20px; background: radial-gradient(ellipse at center, #153b40, #0b1822 75%); overflow: hidden; }
+.iss-hologram-heading { display: flex; justify-content: space-between; align-items: center; gap: 1rem; }
+.iss-hologram-heading h3 { font-size: 1.1rem; margin: 0; }
+.iss-hologram-kicker { color: var(--station-accent); font: 600 .7rem/1.5 monospace; letter-spacing: .18em; margin-bottom: .25rem; }
+.iss-hologram-status { color: var(--station-muted); border: 1px solid var(--station-border); border-radius: 30px; padding: .4rem .75rem; font: .75rem/1.4 monospace; }
+.iss-hologram-status.is-live { color: var(--station-accent); }
+.iss-hologram-status.is-live::before { content: ''; display: inline-block; width: 6px; height: 6px; margin-right: .5rem; border-radius: 50%; background: var(--station-accent); }
+.iss-hologram-stage { position: relative; height: clamp(250px, 35vw, 370px); }
+#iss-globe-canvas { display: block; width: 100%; height: 100%; }
+.iss-scanline { display: none; }
.iss-led-display {
--led-duration: 60s;
- position: relative !important;
- z-index: 6 !important;
+ position: relative;
width: 100%;
height: clamp(3.75rem, 8vh, 5rem);
- margin: 0.4rem 0;
+ margin: 1rem 0;
overflow: hidden;
- border: 1px solid rgba(98, 255, 225, 0.34);
+ border: 1px solid rgba(98, 255, 225, .34);
border-radius: 8px;
- background-color: rgba(2, 10, 9, 0.92);
- background-image: radial-gradient(circle, rgba(98, 255, 225, 0.10) 1px, transparent 1.5px);
- background-size: 5px 5px;
- box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.85), 0 0 16px rgba(98, 255, 225, 0.12);
- -webkit-mask-image: linear-gradient(to right, transparent, #000 3%, #000 97%, transparent);
- mask-image: linear-gradient(to right, transparent, #000 3%, #000 97%, transparent);
-}
-
-.iss-led-display::after {
- content: '';
- position: absolute;
- inset: 0;
- pointer-events: none;
- background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), transparent 22%, transparent 78%, rgba(0, 0, 0, 0.28));
-}
-
-.iss-led-display[hidden] {
- display: none;
-}
-
-.iss-led-display.is-main-led {
- z-index: 1100 !important;
- isolation: isolate;
- flex: 0 0 auto;
- width: 100%;
- margin: 0.2rem 0 0.35rem;
- border-right: 0;
- border-left: 0;
- border-radius: 0;
- background-color: transparent;
-}
-
-body.light-mode .iss-led-display.is-main-led {
- background-color: transparent;
+ background: radial-gradient(circle, rgba(98, 255, 225, .1) 1px, transparent 1.5px) 0 0 / 5px 5px, rgba(2, 10, 9, .92);
+ box-shadow: inset 0 0 20px rgba(0, 0, 0, .85), 0 0 16px rgba(98, 255, 225, .12);
}
-
+.iss-led-display[hidden], .iss-video-panel[hidden] { display: none; }
.iss-led-message {
- position: absolute;
- top: 50%;
- left: 100%;
- width: max-content;
- color: #72ffe4;
- font: 700 clamp(1.25rem, 3.8vh, 2rem)/1 monospace;
- letter-spacing: 0.12em;
- text-transform: uppercase;
- white-space: nowrap;
- text-shadow: 0 0 4px #32d9bd, 0 0 10px rgba(98, 255, 225, 0.72);
+ position: absolute; top: 50%; left: 0; width: max-content;
+ color: #72ffe4; font: 700 clamp(1.25rem, 3.8vh, 2rem)/1 monospace;
+ letter-spacing: .12em; text-transform: uppercase; white-space: nowrap;
+ text-shadow: 0 0 4px #32d9bd, 0 0 10px rgba(98, 255, 225, .72);
transform: translate(0, -50%);
}
-
-.iss-led-message.is-scrolling {
- animation: ledScrollRightToLeft var(--led-duration) linear infinite;
-}
-
-body.light-mode .iss-led-display {
- border-color: rgba(0, 94, 87, 0.36);
- background-color: rgba(229, 245, 240, 0.95);
- background-image: radial-gradient(circle, rgba(0, 94, 87, 0.12) 1px, transparent 1.5px);
- box-shadow: inset 0 0 18px rgba(0, 65, 58, 0.16), 0 0 14px rgba(0, 94, 87, 0.12);
-}
-
-body.light-mode .iss-led-message {
- color: #006b62;
- text-shadow: 0 0 4px rgba(0, 107, 98, 0.35);
-}
-
-.home-hero > .mt-4 {
- margin-top: 0.5rem !important;
-}
-
-.home-hero .mb-4 {
- margin-bottom: 0.55rem !important;
-}
-
-.home-hero h2.mt-4 {
- margin-top: 0.55rem !important;
- margin-bottom: 0.45rem;
-}
-
-.iss-hologram.is-tracking + #iss-result:not(:empty):not(.is-led-report) {
- z-index: 6;
- margin-top: 100vh !important;
- padding: 1rem;
- border: 1px solid rgba(98, 255, 225, 0.28);
- border-radius: 14px;
- background: rgba(5, 18, 19, 0.92);
- box-shadow: 0 14px 38px rgba(0, 0, 0, 0.24);
-}
-
-#iss-result.is-led-report {
- position: absolute !important;
- width: 1px !important;
- height: 1px !important;
- margin: -1px !important;
- padding: 0 !important;
- overflow: hidden !important;
- clip: rect(0 0 0 0) !important;
- clip-path: inset(50%) !important;
- border: 0 !important;
- white-space: nowrap !important;
-}
-
-body.light-mode .iss-hologram.is-tracking + #iss-result:not(:empty):not(.is-led-report) {
- color: #075e5b;
- border-color: rgba(0, 112, 105, 0.25);
- background: rgba(240, 252, 250, 0.94);
- text-shadow: none;
-}
-
-main[role='main'] {
- padding-bottom: 0 !important;
-}
-
-body.light-mode .iss-hologram {
- opacity: 0.34;
-}
-
-body.light-mode .iss-hologram.is-tracking {
- opacity: 0.92;
- filter: saturate(1.15) contrast(1.08) drop-shadow(0 0 20px rgba(0, 112, 105, 0.24));
-}
-
-/* Oslobodi prostor ispod uvećanog globusa prije prikaza footera. */
-body:has(.iss-hologram.is-tracking) .site-footer {
- margin-top: 48vh;
-}
-
-body.light-mode .iss-hologram-kicker,
-body.light-mode .iss-hologram-status,
-body.light-mode .iss-hologram-note {
- color: #075e5b;
-}
-
-body.light-mode .iss-hologram-status {
- border-color: rgba(0, 112, 105, 0.25);
-}
-
-@keyframes issScan {
- to { transform: translateY(540%); }
-}
-
-@keyframes hologramReveal {
- from {
- opacity: 0.28;
- filter: saturate(0.8) brightness(0.85) blur(2px);
- }
-
- to {
- filter: saturate(1.2) brightness(1.08) blur(0);
- }
-}
-
+.iss-led-message.is-scrolling { animation: ledScrollRightToLeft var(--led-duration) linear infinite; }
@keyframes ledScrollRightToLeft {
- from {
- left: 100%;
- transform: translate(0, -50%);
- }
-
- to {
- left: 0;
- transform: translate(-100%, -50%);
- }
-}
-
-@media (max-width: 520px) {
- html,
- body {
- max-width: 100%;
- overflow-x: clip;
- }
-
- .home-hero {
- min-height: calc(75vh - 0.5rem);
- padding-block: 0.4rem;
- overflow: clip;
- }
-
- .iss-hologram-heading {
- align-items: flex-start;
- flex-direction: column;
- }
-
- .iss-hologram,
- .iss-hologram.is-tracking {
- overflow: hidden;
- }
-
- .iss-hologram-stage,
- .iss-hologram.is-tracking .iss-hologram-stage {
- height: 100%;
- max-width: 100%;
- transform: translateY(-2%) scale(1.25);
- transform-origin: 50% 35%;
- }
-
- body:has(.iss-hologram.is-tracking) .site-footer {
- margin-top: 38vh;
- }
-}
-
-@media (max-height: 720px) {
- .home-hero > h1 {
- font-size: 1.8rem;
- }
-
- .home-hero > .lead {
- font-size: 0.95rem;
- }
-
- .home-hero .btn-home {
- margin-bottom: 0.25rem;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .iss-scanline {
- animation: none;
- top: 40%;
- }
-
- .iss-hologram.is-tracking {
- animation: none;
- }
-
- .iss-led-message.is-scrolling {
- animation-duration: calc(var(--led-duration) * 1.75);
- }
+ from { transform: translate(1rem, -50%); }
+ to { transform: translate(-100%, -50%); }
+}
+body.light-mode .iss-led-display { background-color: #e5f5f0; }
+body.light-mode .iss-led-message { color: #006b62; text-shadow: 0 0 4px rgba(0, 107, 98, .35); }
+.station-panel.iss-video-panel {
+ position: fixed;
+ right: max(1rem, env(safe-area-inset-right));
+ bottom: max(1rem, env(safe-area-inset-bottom));
+ z-index: 1040;
+ width: 384px;
+ max-width: calc(100vw - 2rem);
+ max-height: calc(100dvh - 2rem);
+ overflow-y: auto;
+ margin: 0;
+ padding: .75rem;
+ box-shadow: 0 12px 40px rgba(0, 0, 0, .4);
+}
+.iss-video-panel h3 { font-size: .95rem; }
+.iss-video-panel .panel-reference { font-size: .7rem; margin-bottom: .5rem; }
+.iss-video-panel > a { font-size: .75rem; }
+.iss-video-stage { aspect-ratio: 16 / 9; margin-inline: auto; background: #030a10; border-radius: 10px; overflow: hidden; }
+.iss-video-stage iframe { display: block; width: 100%; height: 100%; border: 0; }
+.iss-video-status { color: var(--station-accent); margin-block: .5rem; font-size: .75rem; }
+.starlink-ping-button { margin-left: .6rem; min-height: 44px; }
+.starlink-panel[hidden] { display: none; }
+.starlink-panel { margin-top: 1.5rem; scroll-margin-top: 1rem; background: radial-gradient(ellipse at center, #122d42, var(--station-bg)); }
+.starlink-heading { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
+.starlink-motion { display: inline-flex; align-items: center; gap: .5rem; font-size: .8rem; color: var(--station-muted); min-height: 44px; }
+.starlink-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem; margin: 1rem 0; }
+.starlink-metrics > div { padding: .8rem; background: var(--station-bg); border: 1px solid var(--station-border); border-radius: 12px; }
+.starlink-metrics strong { display: block; font-size: clamp(1.25rem, 3vw, 2rem); color: #71cfff; font-variant-numeric: tabular-nums; }
+.starlink-metrics span, .starlink-note { font-size: .8rem; color: var(--station-muted); }
+.starlink-stage { height: clamp(300px, 45vw, 460px); }
+#starlink-globe { width: 100%; height: 100%; display: block; touch-action: pan-y; cursor: grab; }
+#starlink-globe:active { cursor: grabbing; }
+.starlink-panel .panel-reference { margin-bottom: .5rem; }
+body.light-mode .starlink-panel { background: radial-gradient(ellipse at center, #d9eef9, var(--station-bg)); }
+body.light-mode .starlink-metrics strong { color: #006799; }
+@media (max-width: 650px) {
+ .starlink-heading { align-items: flex-start; flex-direction: column; gap: 0; }
+ .starlink-panel { padding: .8rem; }
+ .starlink-metrics { gap: .4rem; }
+ .starlink-metrics > div { padding: .5rem; }
+ .starlink-ping-button { margin: .5rem 0 0; }
+}
+.iss-hologram-note { font-size: .8rem; color: var(--station-muted); margin: 0; max-width: 45rem; }
+#iss-result:empty { display: none; }
+.home-hero #iss-result { color: #ffd49c; text-shadow: none; font-family: inherit; }
+.iss-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .8rem; margin-top: 1rem; }
+.iss-metric, .station-panel { background: var(--station-bg); border: 1px solid var(--station-border); border-radius: 16px; }
+.iss-metric { padding: 1.1rem; display: flex; flex-direction: column; gap: .45rem; min-width: 0; }
+.iss-metric > span { color: var(--station-muted); font-size: .85rem; }
+.iss-metric strong { color: var(--station-accent); font-size: clamp(1.1rem, 2.4vw, 1.7rem); font-variant-numeric: tabular-nums; line-height: 1.2; overflow-wrap: anywhere; }
+.iss-metric small { color: var(--station-muted); font-size: .72rem; }
+.station-freshness { color: var(--station-muted); font-size: .8rem; margin: 1rem .2rem; }
+.station-warning { background: #382818; color: #ffe0ad; padding: 1rem; border: 1px solid #745125; border-radius: 12px; font-size: .9rem; }
+.station-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
+.station-panel { padding: 1.3rem; min-width: 0; }
+.station-panel h3 { font-size: 1.1rem; font-weight: 700; margin-bottom: .4rem; }
+.panel-reference { color: var(--station-muted); font-size: .75rem; margin-bottom: 1rem; }
+.station-list { list-style: none; padding: 0; margin: 0; }
+.station-list li { padding: .8rem 0; border-top: 1px solid var(--station-border); display: flex; flex-direction: column; gap: .25rem; overflow-wrap: anywhere; }
+.station-list li strong { font-size: .95rem; }
+.station-list li span { color: var(--station-muted); font-size: .8rem; }
+.home-hero a:not(.btn) { color: var(--station-accent); }
+.station-events { margin-top: 1rem; }
+.station-events a { font-size: .95rem; text-decoration: none; }
+.station-events a:hover { text-decoration: underline; }
+body.light-mode .home-hero { --station-bg: #f6fbfc; --station-border: #c6d9df; --station-accent: #00675d; --station-muted: #48636f; }
+body.light-mode .iss-hologram { background: radial-gradient(ellipse at center, #d8f3ee, #eff7fa 75%); }
+body.light-mode .station-warning { background: #fff4df; color: #6c4615; border-color: #e0b875; }
+body.light-mode #iss-result { color: #6c4615; }
+@media (max-width: 650px) {
+ .home-hero { padding-top: 1.25rem; }
+ .iss-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .station-columns { grid-template-columns: 1fr; }
+ .iss-hologram-heading { align-items: flex-start; flex-direction: column; }
+ .iss-hologram { padding: 1rem; }
+ .iss-sound-option { margin-left: .5rem; font-size: .8rem; }
}
diff --git a/Petabit/wwwroot/js/iss-tracker.js b/Petabit/wwwroot/js/iss-tracker.js
index f804eb7..ffcedbf 100644
--- a/Petabit/wwwroot/js/iss-tracker.js
+++ b/Petabit/wwwroot/js/iss-tracker.js
@@ -1,4 +1,4 @@
-document.addEventListener("DOMContentLoaded", function () {
+document.addEventListener("DOMContentLoaded", function () {
const pingButton = document.getElementById("iss-ping-button");
const resultDiv = document.getElementById("iss-result");
const speedDiv = document.getElementById("iss-speed");
@@ -11,7 +11,7 @@
const format = (template, ...values) => values.reduce(
(result, value, index) => result.replaceAll(`{${index}}`, value), template);
- const countries = { SAD: strings.usa, Francuska: strings.france, Rusija: strings.russia };
+ const countries = { SAD: strings.usa, Francuska: strings.france, Rusija: strings.russia, Kanada: strings.canada };
const purposes = { "Posadna letjelica": strings.crewed, "Teretna letjelica": strings.cargo };
function renderList(container, title, items, formatItem) {
@@ -28,7 +28,8 @@
container.replaceChildren(heading, list);
}
- async function pingISS() {
+ async function pingISS(manual = false) {
+ if (pingButton.disabled || (!manual && document.hidden)) return;
pingButton.disabled = true;
resultDiv.textContent = `⏳ ${strings.loading}`;
@@ -54,7 +55,7 @@
const sourceText = document.createTextNode(
`${format(strings.reference, new Date(data.stationStatusUpdatedAt).toLocaleDateString(document.documentElement.lang))}, `);
const sourceLink = document.createElement("a");
- sourceLink.href = "https://www.nasa.gov/international-space-station/space-station-visiting-vehicles/";
+ sourceLink.href = "https://www.nasa.gov/international-space-station/space-station-overview/";
sourceLink.target = "_blank";
sourceLink.rel = "noopener noreferrer";
sourceLink.textContent = "NASA";
@@ -65,7 +66,7 @@
sourceDiv.classList.toggle("text-warning", data.stationStatusIsStale);
sourceDiv.replaceChildren(...sourceNodes);
- pingSound.play();
+ if (manual) pingSound.play().catch(() => {});
} catch (error) {
resultDiv.textContent = `❌ ${strings.error}`;
console.error("Greška:", error);
@@ -74,5 +75,7 @@
pingButton.disabled = false;
}
- pingButton.addEventListener("click", pingISS);
+ pingButton.addEventListener("click", () => pingISS(true));
+ pingISS();
+ setInterval(pingISS, 30000);
});
diff --git a/Petabit/wwwroot/js/iss-video-preview.js b/Petabit/wwwroot/js/iss-video-preview.js
new file mode 100644
index 0000000..0c14fbc
--- /dev/null
+++ b/Petabit/wwwroot/js/iss-video-preview.js
@@ -0,0 +1,157 @@
+// Loaded locally on every visit; YouTube is contacted only after a manual Ping ISS.
+document.addEventListener('DOMContentLoaded', () => {
+ const panel = document.getElementById('iss-video-panel');
+ if (!panel) return;
+ const strings = document.getElementById('iss-led-localization').dataset;
+ const stage = document.getElementById('iss-video-stage');
+ const status = document.getElementById('iss-video-status');
+ let apiPromise;
+ let player;
+ let generation = 0;
+ let playbackTimer;
+ let loadingTimer;
+ let remaining = 5000;
+ let playingSince;
+ let finished = false;
+
+ function loadApi() {
+ if (window.YT?.Player) return Promise.resolve(window.YT);
+ if (apiPromise) return apiPromise;
+ apiPromise = new Promise((resolve, reject) => {
+ const timeout = window.setTimeout(() => reject(new Error('YouTube API timed out')), 15000);
+ window.onYouTubeIframeAPIReady = () => {
+ window.clearTimeout(timeout);
+ resolve(window.YT);
+ };
+ const script = document.createElement('script');
+ script.src = 'https://www.youtube.com/iframe_api';
+ script.async = true;
+ script.onerror = () => {
+ window.clearTimeout(timeout);
+ script.remove();
+ reject(new Error('YouTube API unavailable'));
+ };
+ document.head.append(script);
+ }).catch(error => {
+ apiPromise = undefined;
+ throw error;
+ });
+ return apiPromise;
+ }
+ function clearPlaybackTimer() {
+ window.clearTimeout(playbackTimer);
+ if (playingSince !== undefined) {
+ remaining = Math.max(0, remaining - (performance.now() - playingSince));
+ playingSince = undefined;
+ }
+ }
+ function finish() {
+ clearPlaybackTimer();
+ window.clearTimeout(loadingTimer);
+ finished = true;
+ player?.pauseVideo();
+ status.textContent = strings.videoEnded;
+ panel.dataset.playback = 'finished';
+ }
+ function fail() {
+ clearPlaybackTimer();
+ window.clearTimeout(loadingTimer);
+ finished = true;
+ player?.destroy();
+ player = undefined;
+ stage.replaceChildren();
+ status.textContent = strings.videoError;
+ panel.dataset.playback = 'error';
+ }
+ async function start() {
+ const currentGeneration = ++generation;
+ clearPlaybackTimer();
+ window.clearTimeout(loadingTimer);
+ finished = true;
+ player?.destroy();
+ player = undefined;
+ stage.replaceChildren();
+ panel.hidden = false;
+ panel.dataset.playback = 'loading';
+ status.textContent = strings.videoLoading;
+ remaining = 5000;
+ playingSince = undefined;
+ try {
+ const [YT, response] = await Promise.all([loadApi(), fetch('/Home/VideoSource',
+ { signal: AbortSignal.timeout(15000), cache: 'no-store' })]);
+ if (!response.ok) throw new Error('NASA source unavailable');
+ const source = await response.json();
+ if (currentGeneration !== generation) return;
+ if (!/^[A-Za-z0-9_-]{11}$/.test(source.videoId)) throw new Error('Invalid video ID');
+ const iframe = document.createElement('iframe');
+ iframe.title = strings.videoTitle;
+ iframe.allow = 'autoplay; encrypted-media; picture-in-picture';
+ // YouTube requires an embedding origin (otherwise it may return error 153).
+ iframe.referrerPolicy = 'strict-origin-when-cross-origin';
+ iframe.src = `https://www.youtube-nocookie.com/embed/${source.videoId}?enablejsapi=1&autoplay=1&mute=1&playsinline=1&rel=0&origin=${encodeURIComponent(location.origin)}`;
+ stage.replaceChildren(iframe);
+ finished = false;
+ loadingTimer = window.setTimeout(() => {
+ if (currentGeneration === generation && panel.dataset.playback === 'loading') fail();
+ }, 25000);
+ player = new YT.Player(iframe, {
+ events: {
+ onReady: event => {
+ if (currentGeneration !== generation) return;
+ event.target.mute();
+ event.target.playVideo();
+ },
+ onStateChange: event => {
+ if (currentGeneration !== generation) return;
+ if (finished) {
+ if (event.data === YT.PlayerState.PLAYING) event.target.pauseVideo();
+ return;
+ }
+ clearPlaybackTimer();
+ if (event.data === YT.PlayerState.PLAYING) {
+ window.clearTimeout(loadingTimer);
+ if (remaining <= 0) { finish(); return; }
+ status.textContent = strings.videoPlaying;
+ panel.dataset.playback = 'playing';
+ playingSince = performance.now();
+ playbackTimer = window.setTimeout(finish, remaining);
+ } else if (event.data === YT.PlayerState.BUFFERING) {
+ status.textContent = strings.videoLoading;
+ panel.dataset.playback = 'buffering';
+ window.clearTimeout(loadingTimer);
+ loadingTimer = window.setTimeout(() => {
+ if (currentGeneration === generation && !finished) fail();
+ }, 25000);
+ } else if (event.data === YT.PlayerState.PAUSED) {
+ window.clearTimeout(loadingTimer);
+ status.textContent = strings.videoBlocked;
+ panel.dataset.playback = 'paused';
+ }
+ },
+ onAutoplayBlocked: () => {
+ if (currentGeneration !== generation || finished) return;
+ window.clearTimeout(loadingTimer);
+ status.textContent = strings.videoBlocked;
+ panel.dataset.playback = 'blocked';
+ },
+ onError: () => { if (currentGeneration === generation) fail(); }
+ }
+ });
+ } catch {
+ if (currentGeneration === generation) fail();
+ }
+ }
+ window.addEventListener('iss-ping', start);
+ document.addEventListener('visibilitychange', () => {
+ if (document.hidden && player && !finished) {
+ clearPlaybackTimer();
+ player.pauseVideo();
+ }
+ });
+ window.addEventListener('pagehide', () => {
+ ++generation;
+ clearPlaybackTimer();
+ window.clearTimeout(loadingTimer);
+ player?.destroy();
+ });
+});
diff --git a/Petabit/wwwroot/js/starlink-dashboard.js b/Petabit/wwwroot/js/starlink-dashboard.js
new file mode 100644
index 0000000..be2fa54
--- /dev/null
+++ b/Petabit/wwwroot/js/starlink-dashboard.js
@@ -0,0 +1,158 @@
+const panel = document.getElementById('starlink-panel');
+const button = document.getElementById('starlink-ping-button');
+const canvas = document.getElementById('starlink-globe');
+const ctx = canvas.getContext('2d');
+const motion = document.getElementById('starlink-motion');
+const strings = panel.dataset;
+const status = document.getElementById('starlink-status');
+let worker;
+let frame;
+let animation;
+let pending = false;
+let rotation = -.28;
+let frozenAt;
+let pointerX;
+const earthKm = 6371;
+const continents = [
+ [[72,-168],[60,-128],[49,-125],[34,-117],[17,-96],[25,-82],[43,-70],[59,-64],[73,-105],[72,-168]],
+ [[12,-81],[-5,-81],[-35,-72],[-55,-68],[-34,-52],[-5,-35],[12,-66],[12,-81]],
+ [[71,-10],[51,3],[36,-6],[35,10],[47,40],[60,80],[72,105],[61,163],[35,126],[10,105],[7,80],[30,50],[45,28],[71,-10]],
+ [[36,-17],[37,10],[31,32],[2,51],[-25,35],[-35,18],[-17,12],[5,-1],[15,-17],[36,-17]],
+ [[-11,113],[-25,113],[-35,117],[-39,146],[-17,146],[-11,132],[-11,113]],
+ [[83,-52],[68,-54],[60,-44],[64,-25],[76,-18],[83,-52]]
+];
+function resize() {
+ const ratio = Math.min(devicePixelRatio || 1, 2);
+ canvas.width = Math.round(canvas.clientWidth * ratio);
+ canvas.height = Math.round(canvas.clientHeight * ratio);
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
+}
+function draw() {
+ const w = canvas.clientWidth, h = canvas.clientHeight;
+ const cx = w / 2, cy = h / 2, radius = Math.min(w, h) * .385;
+ const light = document.body.classList.contains('light-mode');
+ const cos = Math.cos(rotation), sin = Math.sin(rotation);
+ const project = (lat, lon) => {
+ const phi = lat * Math.PI / 180, lambda = lon * Math.PI / 180 + rotation;
+ return { x: cx + radius * Math.cos(phi) * Math.sin(lambda),
+ y: cy - radius * Math.sin(phi), front: Math.cos(phi) * Math.cos(lambda) >= 0 };
+ };
+ const line = (points, color) => {
+ ctx.strokeStyle = color;
+ ctx.lineWidth = .7;
+ ctx.beginPath();
+ let started = false;
+ for (const [lat, lon] of points) {
+ const p = project(lat, lon);
+ if (!p.front) { started = false; continue; }
+ if (!started) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y);
+ started = true;
+ }
+ ctx.stroke();
+ };
+ ctx.clearRect(0, 0, w, h);
+ const glow = ctx.createRadialGradient(cx, cy, radius * .6, cx, cy, radius * 1.3);
+ glow.addColorStop(0, light ? '#b6e3f3' : '#15384f');
+ glow.addColorStop(1, 'rgba(0,0,0,0)');
+ ctx.fillStyle = glow;
+ ctx.fillRect(0, 0, w, h);
+ // Rear satellites are visible only beyond Earth's limb.
+ const dots = [];
+ if (frame) {
+ const dt = Math.min(2, Math.max(0, ((motion.checked ? Date.now() : frozenAt) - frame.timestamp) / 1000));
+ for (let i = 0; i < frame.count * 6; i += 6) {
+ const p = frame.packed;
+ const x = p[i] + p[i + 3] * dt, y = p[i + 1] + p[i + 4] * dt, z = p[i + 2] + p[i + 5] * dt;
+ const depth = x * cos - y * sin;
+ const dx = (y * cos + x * sin) / earthKm * radius, dy = -z / earthKm * radius;
+ if (depth < 0 && dx * dx + dy * dy < radius * radius) continue;
+ dots.push({ x: cx + dx, y: cy + dy, front: depth >= 0 });
+ }
+ }
+ const paintDots = front => {
+ ctx.fillStyle = front ? (light ? '#006799' : '#8ad9ff') : (light ? '#87aabd' : '#355d7d');
+ ctx.beginPath();
+ for (const p of dots) if (p.front === front) { ctx.moveTo(p.x + 1.15, p.y); ctx.arc(p.x, p.y, 1.15, 0, Math.PI * 2); }
+ ctx.fill();
+ };
+ paintDots(false);
+ ctx.fillStyle = light ? '#e5f6fa' : '#091b2b';
+ ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.fill();
+ ctx.strokeStyle = light ? '#429ab7' : '#4594b5'; ctx.lineWidth = 1.5; ctx.stroke();
+ for (let lat = -60; lat <= 60; lat += 30)
+ line(Array.from({ length: 91 }, (_, i) => [lat, i * 4 - 180]), light ? '#add1db' : '#1d465d');
+ for (let lon = -180; lon < 180; lon += 30)
+ line(Array.from({ length: 61 }, (_, i) => [i * 3 - 90, lon]), light ? '#add1db' : '#1d465d');
+ continents.forEach(points => line(points, light ? '#418d88' : '#61ada6'));
+ paintDots(true);
+ if (motion.checked && !document.hidden && !panel.hidden && frame) animation = requestAnimationFrame(draw);
+}
+function run() {
+ cancelAnimationFrame(animation);
+ if (!worker || panel.hidden) return;
+ if (document.hidden || !motion.checked) {
+ frozenAt = Date.now();
+ worker.postMessage({ type: 'stop' });
+ } else worker.postMessage({ type: 'start' });
+ status.textContent = motion.checked ? strings.motion : strings.paused;
+ draw();
+}
+button.addEventListener('click', async () => {
+ if (pending) return;
+ pending = true;
+ button.disabled = true;
+ panel.hidden = false;
+ panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ status.textContent = strings.loading;
+ const warning = document.getElementById('starlink-warning');
+ warning.hidden = true;
+ try {
+ const response = await fetch('/Home/StarlinkData', { cache: 'no-store', signal: AbortSignal.timeout(55000) });
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ const data = await response.json();
+ const number = new Intl.NumberFormat(strings.locale);
+ for (const [id, value] of [['operational', data.operationalCount], ['orbit', data.onOrbitCount], ['partial', data.partiallyOperationalCount]])
+ document.getElementById(`starlink-${id}`).textContent = number.format(value);
+ const date = value => new Date(value).toLocaleString(strings.locale);
+ document.getElementById('starlink-freshness').textContent = `${strings.updated}: ${date(data.retrievedAt)} · ${strings.epoch}: ${date(data.oldestEpoch)} – ${date(data.newestEpoch)}`;
+ warning.textContent = data.isStale || data.refreshFailed ? strings.stale : '';
+ if (data.persistenceFailed) warning.textContent += ` ${document.getElementById('iss-led-localization').dataset.persistence}`;
+ warning.hidden = !warning.textContent;
+ worker?.terminate();
+ cancelAnimationFrame(animation);
+ frame = undefined;
+ worker = new Worker(new URL('./starlink-worker.js', import.meta.url), { type: 'module' });
+ worker.onmessage = ({ data: next }) => {
+ frame = next;
+ if (!next.count) status.textContent = strings.error;
+ frozenAt = next.timestamp;
+ canvas.dataset.mappedCount = String(next.count);
+ canvas.dataset.positionTime = String(next.timestamp);
+ document.getElementById('starlink-coverage').textContent = `${strings.mapped}: ${number.format(next.count)} / ${number.format(data.operationalCount)}`;
+ cancelAnimationFrame(animation);
+ draw();
+ if (!motion.checked || document.hidden) worker.postMessage({ type: 'stop' });
+ };
+ worker.onerror = () => { status.textContent = strings.error; worker?.terminate(); cancelAnimationFrame(animation); };
+ resize();
+ worker.postMessage({ type: 'start', elements: data.elements });
+ status.textContent = motion.checked ? strings.motion : strings.paused;
+ } catch {
+ status.textContent = strings.error;
+ if (frame) { warning.textContent = strings.stale; warning.hidden = false; }
+ } finally { pending = false; button.disabled = false; }
+});
+motion.addEventListener('change', run);
+document.addEventListener('visibilitychange', run);
+window.addEventListener('resize', () => { if (!panel.hidden) { resize(); cancelAnimationFrame(animation); draw(); } });
+document.getElementById('toggle-dark-mode')?.addEventListener('click', () => { cancelAnimationFrame(animation); draw(); });
+canvas.addEventListener('pointerdown', event => { pointerX = event.clientX; canvas.setPointerCapture(event.pointerId); });
+canvas.addEventListener('pointermove', event => {
+ if (pointerX === undefined) return;
+ rotation += (event.clientX - pointerX) * .006;
+ pointerX = event.clientX;
+ cancelAnimationFrame(animation); draw();
+});
+canvas.addEventListener('pointerup', () => { pointerX = undefined; });
+canvas.addEventListener('pointercancel', () => { pointerX = undefined; });
+window.addEventListener('pagehide', () => { worker?.terminate(); cancelAnimationFrame(animation); });
diff --git a/Petabit/wwwroot/js/starlink-orbits.mjs b/Petabit/wwwroot/js/starlink-orbits.mjs
new file mode 100644
index 0000000..9239a5f
--- /dev/null
+++ b/Petabit/wwwroot/js/starlink-orbits.mjs
@@ -0,0 +1,30 @@
+import { json2satrec } from '../lib/satellite-js/dist/io.js';
+import { propagate, gstime } from '../lib/satellite-js/dist/propagation.js';
+import { eciToEcf } from '../lib/satellite-js/dist/transforms.js';
+
+export function createRecords(elements) {
+ return elements.map(element => {
+ try { return json2satrec(element); } catch { return null; }
+ }).filter(Boolean);
+}
+
+// Earth-fixed km and km/s. Main-thread interpolation keeps the animation smooth
+// while SGP4 runs once a second in a worker, including Earth's own rotation.
+export function positionsAt(records, timestamp) {
+ const time = new Date(timestamp);
+ const sidereal = gstime(time);
+ const packed = new Float32Array(records.length * 6);
+ let count = 0;
+ for (const record of records) {
+ const state = propagate(record, time);
+ if (!state?.position || !state.velocity) continue;
+ const p = eciToEcf(state.position, sidereal);
+ const v = eciToEcf(state.velocity, sidereal);
+ if (![p.x, p.y, p.z, v.x, v.y, v.z].every(Number.isFinite)
+ || Math.hypot(p.x, p.y, p.z) < 6371) continue;
+ packed.set([p.x, p.y, p.z, v.x + 7.292115e-5 * p.y,
+ v.y - 7.292115e-5 * p.x, v.z], count * 6);
+ count++;
+ }
+ return { timestamp, count, packed: packed.subarray(0, count * 6) };
+}
diff --git a/Petabit/wwwroot/js/starlink-worker.js b/Petabit/wwwroot/js/starlink-worker.js
new file mode 100644
index 0000000..54f7664
--- /dev/null
+++ b/Petabit/wwwroot/js/starlink-worker.js
@@ -0,0 +1,14 @@
+import { createRecords, positionsAt } from './starlink-orbits.mjs';
+let records = [];
+let timer;
+function update() {
+ const frame = positionsAt(records, Date.now());
+ self.postMessage(frame, [frame.packed.buffer]);
+ timer = setTimeout(update, 1000);
+}
+self.onmessage = ({ data }) => {
+ clearTimeout(timer);
+ if (data.type === 'stop') return;
+ if (data.elements) records = createRecords(data.elements);
+ if (records.length) update();
+};
diff --git a/Petabit/wwwroot/js/station-dashboard.js b/Petabit/wwwroot/js/station-dashboard.js
new file mode 100644
index 0000000..78ddefb
--- /dev/null
+++ b/Petabit/wwwroot/js/station-dashboard.js
@@ -0,0 +1,155 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const root = document.getElementById('station-dashboard');
+ if (!root) return;
+ const strings = document.getElementById('iss-led-localization').dataset;
+ const button = document.getElementById('iss-ping-button');
+ const countries = { SAD: strings.countryUsa, Francuska: strings.countryFrance, Rusija: strings.countryRussia, Kanada: strings.countryCanada };
+ const purposes = { 'Posadna letjelica': strings.purposeCrewed, 'Teretna letjelica': strings.purposeCargo };
+ const date = value => new Date(value).toLocaleString(strings.locale, { dateStyle: 'medium', timeStyle: 'short' });
+ const write = (id, text) => { document.getElementById(id).textContent = text; };
+ let busy = false;
+ let positionTime;
+ let lastStationVersion;
+ let latestPosition;
+ let latestStation;
+ let positionAvailable = false;
+ const led = document.getElementById('iss-led-display');
+ const ledMessage = document.getElementById('iss-led-message');
+ function renderLed(restart = false) {
+ if (led.hidden) return;
+ const parts = [positionAvailable ? 'ISS LIVE' : strings.unavailable];
+ if (latestPosition) parts.push(`${strings.location}: ${latestPosition.latitude.toFixed(2)}° / ${latestPosition.longitude.toFixed(2)}°`,
+ `${strings.speed}: ${latestPosition.speed.toFixed(0)} KM/H`);
+ if (latestStation) {
+ parts.push(`${strings.astronauts}: ${latestStation.astronautCount}`,
+ `${strings.crew}: ${latestStation.astronauts.map(p => `${p.name} — ${countries[p.country] ?? p.country} (${p.agency})`).join(' | ')}`,
+ `${strings.docked}: ${latestStation.dockedVehicles.map(v => `${v.name} — ${purposes[v.purpose] ?? v.purpose}`).join(' | ')}`,
+ latestStation.stationStatusCheckedAt ? `${strings.checked}: ${date(latestStation.stationStatusCheckedAt)}` : strings.stale,
+ `${strings.source}: NASA`);
+ if (latestStation.stationStatusNeedsReview) parts.push(strings.review);
+ if (latestStation.stationStatusIsStale || latestStation.stationStatusRefreshFailed) parts.push(strings.stale);
+ }
+ ledMessage.textContent = parts.join(' • ');
+ if (restart) {
+ led.style.setProperty('--led-duration', `${Math.max(20, (ledMessage.scrollWidth + led.clientWidth) / 85)}s`);
+ ledMessage.classList.remove('is-scrolling');
+ void ledMessage.offsetWidth;
+ ledMessage.classList.add('is-scrolling');
+ }
+ }
+ function link(url, text) {
+ const a = document.createElement('a');
+ try {
+ const target = new URL(url);
+ if (target.protocol !== 'https:' || target.hostname !== 'www.nasa.gov') return document.createTextNode(text);
+ a.href = target.href;
+ } catch { return document.createTextNode(text); }
+ a.textContent = text;
+ a.target = '_blank';
+ a.rel = 'noopener noreferrer';
+ return a;
+ }
+ function list(id, items, makeItem) {
+ document.getElementById(id).replaceChildren(...items.map(item => {
+ const li = document.createElement('li');
+ makeItem(li, item);
+ return li;
+ }));
+ }
+ function details(li, name, detail) {
+ const strong = document.createElement('strong');
+ strong.textContent = name;
+ const small = document.createElement('span');
+ small.textContent = detail;
+ li.append(strong, small);
+ }
+ function renderStation(data) {
+ write('metric-crew', data.astronautCount);
+ write('metric-vehicles', data.dockedVehicles.length);
+ write('station-freshness', data.stationStatusCheckedAt
+ ? `${strings.checked}: ${date(data.stationStatusCheckedAt)} · NASA`
+ : strings.stale);
+ const warning = document.getElementById('station-warning');
+ warning.textContent = [data.stationStatusIsStale || data.stationStatusRefreshFailed ? strings.stale : '',
+ data.stationStatusNeedsReview ? strings.review : '', data.stationStatusPersistenceFailed ? strings.persistence : ''].filter(Boolean).join(' ');
+ warning.hidden = !warning.textContent;
+ const version = JSON.stringify([data.astronauts, data.dockedVehicles, data.events, data.crewUpdatedAt, data.vehiclesUpdatedAt]);
+ if (version === lastStationVersion) return;
+ lastStationVersion = version;
+ list('station-crew', data.astronauts, (li, person) => details(li, person.name,
+ [countries[person.country] ?? person.country, person.agency, person.mission].filter(Boolean).join(' · ')));
+ list('station-vehicles', data.dockedVehicles, (li, vehicle) => details(li, vehicle.name,
+ `${purposes[vehicle.purpose] ?? vehicle.purpose} · ${vehicle.operator}`));
+ for (const [id, time, source] of [['crew-reference', data.crewUpdatedAt, data.crewSource],
+ ['vehicles-reference', data.vehiclesUpdatedAt, data.stationStatusSource]]) {
+ const timestamp = id === 'vehicles-reference'
+ ? new Date(time).toLocaleDateString(strings.locale, { dateStyle: 'medium', timeZone: 'UTC' }) : date(time);
+ document.getElementById(id).replaceChildren(document.createTextNode(`${strings.confirmed}: ${timestamp} · `), link(source, 'NASA'));
+ }
+ list('station-events', data.events.slice(0, 8), (li, event) => {
+ const kind = { arrival: strings.arrival, departure: strings.departure, 'crew-arrival': strings.crewArrival, review: strings.review, news: strings.news }[event.kind] ?? strings.news;
+ const meta = document.createElement('span');
+ meta.textContent = `${date(event.publishedAt)} · ${kind}`;
+ li.append(meta, link(event.sourceUrl, event.title));
+ });
+ if (!data.events.length) write('station-events', strings.noEvents);
+ }
+ async function get(url) {
+ const response = await fetch(url, { cache: 'no-store', signal: AbortSignal.timeout(15000) });
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ return response.json();
+ }
+ async function refresh(manual = false) {
+ if (busy || (!manual && document.hidden)) return;
+ if (manual) {
+ document.querySelector('main[role="main"]').prepend(led);
+ led.hidden = false;
+ ledMessage.textContent = strings.loading;
+ ledMessage.classList.add('is-scrolling');
+ window.dispatchEvent(new Event('iss-ping'));
+ }
+ busy = true;
+ button.disabled = true;
+ button.textContent = strings.loading;
+ const results = await Promise.allSettled([get('/Home/Data'), get('/Home/StationData')]);
+ const position = results[0];
+ const state = results[1];
+ if (position.status === 'fulfilled') {
+ const data = position.value;
+ latestPosition = data;
+ positionAvailable = true;
+ positionTime = data.positionUpdatedAt;
+ write('metric-position', `${data.latitude.toFixed(2)}° / ${data.longitude.toFixed(2)}°`);
+ write('metric-speed', new Intl.NumberFormat(strings.locale, { maximumFractionDigits: 0 }).format(data.speed));
+ write('position-freshness', `${strings.positionUpdated}: ${date(positionTime)}`);
+ write('iss-result', '');
+ window.dispatchEvent(new CustomEvent('iss-position', { detail: { latitude: data.latitude, longitude: data.longitude } }));
+ if (manual && document.getElementById('iss-sound-enabled').checked)
+ document.getElementById('ping-sound').play().catch(() => {});
+ } else {
+ positionAvailable = false;
+ write('iss-result', strings.unavailable);
+ write('position-freshness', positionTime ? `${strings.unavailable} · ${date(positionTime)}` : strings.unavailable);
+ document.getElementById('iss-hologram-status').textContent = strings.unavailable;
+ document.getElementById('iss-hologram-status').classList.remove('is-live');
+ window.dispatchEvent(new Event('iss-position-unavailable'));
+ }
+ if (state.status === 'fulfilled') {
+ latestStation = state.value;
+ renderStation(state.value);
+ }
+ else {
+ const warning = document.getElementById('station-warning');
+ warning.textContent = strings.stale;
+ warning.hidden = false;
+ }
+ renderLed(manual);
+ button.disabled = false;
+ button.textContent = strings.refresh;
+ busy = false;
+ }
+ button.addEventListener('click', () => refresh(true));
+ document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
+ refresh();
+ window.setInterval(refresh, 30000);
+});
diff --git a/Petabit/wwwroot/lib/satellite-js/LICENSE.md b/Petabit/wwwroot/lib/satellite-js/LICENSE.md
new file mode 100644
index 0000000..a2d06db
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (C) 2013 Shashwat Kandadai, UCSC Jack Baskin School of Engineering
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Petabit/wwwroot/lib/satellite-js/NOTICE.md b/Petabit/wwwroot/lib/satellite-js/NOTICE.md
new file mode 100644
index 0000000..10dfed6
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/NOTICE.md
@@ -0,0 +1 @@
+Satellite.js 7.1.0, https://github.com/shashwatak/satellite-js, MIT. Vendored from the npm registry package. The tracker uses the JavaScript SGP4 modules directly; WebAssembly is not used.
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/common-types.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/common-types.d.ts
new file mode 100644
index 0000000..d2320b2
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/common-types.d.ts
@@ -0,0 +1,219 @@
+/**
+ * Type alias documents units are kilometer (km)
+ */
+export type Kilometer = number;
+/**
+ * Type alias documents units are Astronomical Units (AU)
+ */
+export type AU = number;
+/**
+ * Type alias documents units are kilometer per second (km/s)
+ */
+export type KilometerPerSecond = number;
+/**
+ * Type alias documents units are radians
+ */
+export type Radians = number;
+/**
+ * Type alias documents units are degrees
+ */
+export type Degrees = number;
+/**
+ * Type alias documents units for mean motion are radians per minute
+ */
+export type RadiansPerMinute = number;
+/**
+ * Type alias documents units for semi-major axis (earth radii)
+ */
+export type EarthRadii = number;
+/**
+ * Coordinate frame Earth Centered Inertial (ECI)
+ * https://en.wikipedia.org/wiki/Earth-centered_inertial
+ */
+export interface EciVec3 {
+ x: T;
+ y: T;
+ z: T;
+}
+/**
+ * Coordinate frame Earth Centered Fixed (ECF)
+ * https://en.wikipedia.org/wiki/ECEF
+ */
+export interface EcfVec3 {
+ x: T;
+ y: T;
+ z: T;
+}
+/**
+ * A set of "singly averaged mean elements" that describe shape of the
+ * satellite’s orbit at the propagation date. They are averaged
+ * with respect to the mean anomaly and include the effects of secular
+ * gravity, atmospheric drag, and - in Deep Space mode - of those
+ * pertubations from the Sun and Moon that SGP4 averages over an entire
+ * revolution of each of those bodies. They omit both the shorter-term
+ * and longer-term periodic pertubations from the Sun and Moon that
+ * SGP4 applies right before computing each position.
+ */
+export interface MeanElements {
+ /**
+ * Average semi-major axis (earth radii).
+ */
+ am: EarthRadii;
+ /**
+ * Average eccentricity.
+ */
+ em: number;
+ /**
+ * Average inclination (radians).
+ */
+ im: Radians;
+ /**
+ * Average right ascension of ascending node (radians).
+ */
+ Om: Radians;
+ /**
+ * Average argument of perigee (radians).
+ */
+ om: Radians;
+ /**
+ * Average mean anomaly (radians).
+ */
+ mm: Radians;
+ /**
+ * Average mean motion (radians/minute).
+ */
+ nm: RadiansPerMinute;
+}
+/**
+ * The position_velocity result is a key-value pair of ECI coordinates.
+ * These are the base results from which all other coordinates are derived.
+ * If there is an error the position and velocity will be false.
+ * The meanElements are the averaged elements of the orbit at the
+ * moment of propagation.
+ */
+export interface PositionAndVelocity {
+ position: EciVec3;
+ velocity: EciVec3;
+ meanElements: MeanElements;
+}
+/**
+ * https://en.wikipedia.org/wiki/Geographic_coordinate_system#Latitude_and_longitude
+ */
+export interface GeodeticLocation {
+ longitude: Radians;
+ latitude: Radians;
+ height: Kilometer;
+}
+/**
+ * You will need GMST for some of the coordinate transforms.
+ * GMST - Greenwich Mean Sidereal Time
+ * http://en.wikipedia.org/wiki/Sidereal_time#Definition
+ */
+export type GMSTime = number;
+/**
+ * https://en.wikipedia.org/wiki/Azimuth
+ */
+export interface LookAngles {
+ azimuth: Radians;
+ elevation: Radians;
+ rangeSat: Kilometer;
+}
+type StartsWith = `${T}${string}`;
+/**
+ * This is the base interface for the OMM JSON object as specified in Orbit Data Messages
+ * recommended standard, version 3.0.
+ *
+ * Note that this is not a 1:1 mapping. Only the fields that are necessary to propagate
+ * a satellite orbit are made required. For example, CCSDS_OMM_VERS is required by the spec,
+ * but is not present in Celestrak OMM output, and is not required to propagate the satellite,
+ * so it is made optional here.
+ *
+ * Numeric fields may be represented as strings or numbers in the original json, depending on
+ * the source. This is because the spec doesn't specify the type, and different sources use
+ * different types: at the time of writing, Celestrak uses numbers, while SpaceTrack uses strings.
+ */
+export interface OMMJsonObjectV3 {
+ /**
+ * The version is restricted in TypeScript to only among the ones this library supports.
+ * Versions are incremented on breaking changes, so it is adviced you validate this field.
+ *
+ * NOTE: This is required field in OMM spec, but Celestrak omits it, so we make it optional
+ * and assume version 3.0 if not present.
+ */
+ CCSDS_OMM_VERS?: StartsWith<'3.'>;
+ COMMENT?: string;
+ CLASSIFICATION?: string;
+ OBJECT_NAME: string;
+ /**
+ * Recommended, but not required, to be an International spacecraft designator.
+ */
+ OBJECT_ID: string;
+ /**
+ * NOTE: This is required field in OMM spec, but Celestrak omits it, so we make it optional.
+ * If present, it is restricted to only the value this library supports.
+ */
+ CENTER_NAME?: 'EARTH';
+ /**
+ * NOTE: This is required field in OMM spec, but Celestrak omits it, so we make it optional.
+ * If present, it is restricted to only 'TEME' as it is the only value that can come with
+ * SGP ephemeris type.
+ */
+ REF_FRAME?: 'TEME';
+ /**
+ * NOTE: This is required field in OMM spec, but Celestrak omits it, so we make it optional
+ */
+ REF_FRAME_EPOCH?: string;
+ /**
+ * NOTE: This is required field in OMM spec, but Celestrak omits it, so we make it optional
+ */
+ TIME_SYSTEM?: 'UTC';
+ /**
+ * NOTE: This is required field in OMM spec, but Celestrak omits it, so we make it optional
+ */
+ MEAN_ELEMENT_THEORY?: 'SGP4';
+ /**
+ * NOTE: This is required field in OMM spec, but Celestrak omits it, so we make it optional
+ */
+ CREATION_DATE?: string;
+ /**
+ * NOTE: This is required field in OMM spec, but Celestrak omits it, so we make it optional
+ */
+ ORIGINATOR?: string;
+ /**
+ * ISO datetime
+ */
+ EPOCH: string;
+ MEAN_MOTION: string | number;
+ ECCENTRICITY: string | number;
+ /**
+ * Degrees
+ */
+ INCLINATION: number | string;
+ RA_OF_ASC_NODE: number | string;
+ /**
+ * Degrees
+ */
+ ARG_OF_PERICENTER: number | string;
+ /**
+ * Degrees
+ */
+ MEAN_ANOMALY: number | string;
+ /**
+ * The only value supported by this library
+ */
+ EPHEMERIS_TYPE?: 0 | '0';
+ CLASSIFICATION_TYPE?: 'U' | 'C';
+ NORAD_CAT_ID: string | number;
+ ELEMENT_SET_NO: string | number;
+ REV_AT_EPOCH?: string | number;
+ BSTAR: string | number;
+ MEAN_MOTION_DOT: string | number;
+ MEAN_MOTION_DDOT: string | number;
+ /**
+ * This handles additional metadata, such as OBJECT_TYPE, COUNTRY_CODE etc
+ */
+ [key: string]: unknown;
+}
+export type OMMJsonObject = OMMJsonObjectV3;
+export type SupportedOMMVersion = NonNullable;
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/common-types.js b/Petabit/wwwroot/lib/satellite-js/dist/common-types.js
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/common-types.js
@@ -0,0 +1 @@
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/constants.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/constants.d.ts
new file mode 100644
index 0000000..59491fd
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/constants.d.ts
@@ -0,0 +1,16 @@
+export declare const pi: number;
+export declare const twoPi: number;
+export declare const deg2rad: number;
+export declare const rad2deg: number;
+export declare const minutesPerDay = 1440;
+export declare const mu = 398600.8;
+export declare const earthRadius = 6378.135;
+export declare const xke: number;
+export declare const vkmpersec: number;
+export declare const tumin: number;
+export declare const j2 = 0.001082616;
+export declare const j3 = -0.00000253881;
+export declare const j4 = -0.00000165597;
+export declare const j3oj2: number;
+export declare const x2o3: number;
+export declare const xpdotp: number;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/constants.js b/Petabit/wwwroot/lib/satellite-js/dist/constants.js
new file mode 100644
index 0000000..b526e66
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/constants.js
@@ -0,0 +1,16 @@
+export const pi = Math.PI;
+export const twoPi = pi * 2;
+export const deg2rad = pi / 180.0;
+export const rad2deg = 180 / pi;
+export const minutesPerDay = 1440.0;
+export const mu = 398600.8; // in km3 / s2
+export const earthRadius = 6378.135; // in km
+export const xke = 60.0 / Math.sqrt((earthRadius * earthRadius * earthRadius) / mu);
+export const vkmpersec = (earthRadius * xke) / 60.0;
+export const tumin = 1.0 / xke;
+export const j2 = 0.001082616;
+export const j3 = -0.00000253881;
+export const j4 = -0.00000165597;
+export const j3oj2 = j3 / j2;
+export const x2o3 = 2.0 / 3.0;
+export const xpdotp = 1440.0 / (2.0 * pi); // 229.1831180523293;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/dopplerFactor.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/dopplerFactor.d.ts
new file mode 100644
index 0000000..89e9adc
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/dopplerFactor.d.ts
@@ -0,0 +1,8 @@
+import type { EcfVec3, Kilometer, KilometerPerSecond } from './common-types.js';
+/**
+ * Negative range rate means the satellite is moving towards the observer and
+ * its frequency is shifted higher because 1 minus a negative range rate is
+ * positive. If the range rate is positive, the satellite is moving away from
+ * the observer and its frequency is shifted lower.
+ */
+export declare function dopplerFactor(observerCoordsEcf: EcfVec3, positionEcf: EcfVec3, velocityEcf: EcfVec3): number;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/dopplerFactor.js b/Petabit/wwwroot/lib/satellite-js/dist/dopplerFactor.js
new file mode 100644
index 0000000..51d8038
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/dopplerFactor.js
@@ -0,0 +1,21 @@
+const earthRotation = 7.292115e-5;
+const c = 299792.458; // Speed of light in km/s
+/**
+ * Negative range rate means the satellite is moving towards the observer and
+ * its frequency is shifted higher because 1 minus a negative range rate is
+ * positive. If the range rate is positive, the satellite is moving away from
+ * the observer and its frequency is shifted lower.
+ */
+export function dopplerFactor(observerCoordsEcf, positionEcf, velocityEcf) {
+ const rangeX = positionEcf.x - observerCoordsEcf.x;
+ const rangeY = positionEcf.y - observerCoordsEcf.y;
+ const rangeZ = positionEcf.z - observerCoordsEcf.z;
+ const length = Math.sqrt(rangeX ** 2 + rangeY ** 2 + rangeZ ** 2);
+ const rangeVel = {
+ x: velocityEcf.x + earthRotation * observerCoordsEcf.y,
+ y: velocityEcf.y - earthRotation * observerCoordsEcf.x,
+ z: velocityEcf.z,
+ };
+ const rangeRate = (rangeX * rangeVel.x + rangeY * rangeVel.y + rangeZ * rangeVel.z) / length;
+ return 1 - rangeRate / c;
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/ext.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/ext.d.ts
new file mode 100644
index 0000000..1003f2c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/ext.d.ts
@@ -0,0 +1,19 @@
+export type JDay = number;
+export declare function days2mdhms(year: number, days: number): {
+ mon: number;
+ day: number;
+ hr: number;
+ minute: number;
+ sec: number;
+};
+export declare function jday(year: Date): JDay;
+export declare function jday(year: number, mon: number, day: number, hr: number, minute: number, sec: number, msec?: number): JDay;
+export declare function invjday(jd: JDay, asArray: true): [
+ year: number,
+ mon: number,
+ day: number,
+ hr: number,
+ minute: number,
+ sec: number
+];
+export declare function invjday(jd: JDay, asArray?: false): Date;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/ext.js b/Petabit/wwwroot/lib/satellite-js/dist/ext.js
new file mode 100644
index 0000000..8051344
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/ext.js
@@ -0,0 +1,152 @@
+/* -----------------------------------------------------------------------------
+ *
+ * procedure days2mdhms
+ *
+ * this procedure converts the day of the year, days, to the equivalent month
+ * day, hour, minute and second.
+ *
+ * algorithm : set up array for the number of days per month
+ * find leap year - use 1900 because 2000 is a leap year
+ * loop through a temp value while the value is < the days
+ * perform int conversions to the correct day and month
+ * convert remainder into h m s using type conversions
+ *
+ * author : david vallado 719-573-2600 1 mar 2001
+ *
+ * inputs description range / units
+ * year - year 1900 .. 2100
+ * days - julian day of the year 0.0 .. 366.0
+ *
+ * outputs :
+ * mon - month 1 .. 12
+ * day - day 1 .. 28,29,30,31
+ * hr - hour 0 .. 23
+ * min - minute 0 .. 59
+ * sec - second 0.0 .. 59.999
+ *
+ * locals :
+ * dayofyr - day of year
+ * temp - temporary extended values
+ * inttemp - temporary int value
+ * i - index
+ * lmonth[12] - int array containing the number of days per month
+ *
+ * coupling :
+ * none.
+ * --------------------------------------------------------------------------- */
+export function days2mdhms(year, days) {
+ const lmonth = [
+ 31,
+ year % 4 === 0 ? 29 : 28,
+ 31,
+ 30,
+ 31,
+ 30,
+ 31,
+ 31,
+ 30,
+ 31,
+ 30,
+ 31,
+ ];
+ const dayofyr = Math.floor(days);
+ // ----------------- find month and day of month ----------------
+ let i = 1;
+ let inttemp = 0;
+ // i starts from 1 so no null check is needed
+ // biome-ignore-start lint/style/noNonNullAssertion: index arithmetic
+ while (dayofyr > inttemp + lmonth[i - 1] && i < 12) {
+ inttemp += lmonth[i - 1];
+ // biome-ignore-end lint/style/noNonNullAssertion: index arithmetic
+ i += 1;
+ }
+ const mon = i;
+ const day = dayofyr - inttemp;
+ // ----------------- find hours minutes and seconds -------------
+ let temp = (days - dayofyr) * 24.0;
+ const hr = Math.floor(temp);
+ temp = (temp - hr) * 60.0;
+ const minute = Math.floor(temp);
+ const sec = (temp - minute) * 60.0;
+ return {
+ mon,
+ day,
+ hr,
+ minute,
+ sec,
+ };
+}
+/* -----------------------------------------------------------------------------
+ *
+ * procedure jday
+ *
+ * this procedure finds the julian date given the year, month, day, and time.
+ * the julian date is defined by each elapsed day since noon, jan 1, 4713 bc.
+ *
+ * algorithm : calculate the answer in one step for efficiency
+ *
+ * author : david vallado 719-573-2600 1 mar 2001
+ *
+ * inputs description range / units
+ * year - year 1900 .. 2100
+ * mon - month 1 .. 12
+ * day - day 1 .. 28,29,30,31
+ * hr - universal time hour 0 .. 23
+ * min - universal time min 0 .. 59
+ * sec - universal time sec 0.0 .. 59.999
+ *
+ * outputs :
+ * jd - julian date days from 4713 bc
+ *
+ * locals :
+ * none.
+ *
+ * coupling :
+ * none.
+ *
+ * references :
+ * vallado 2007, 189, alg 14, ex 3-14
+ *
+ * --------------------------------------------------------------------------- */
+function jdayInternal(year, mon, day, hr, minute, sec, msec = 0) {
+ return (367.0 * year -
+ Math.floor(7 * (year + Math.floor((mon + 9) / 12.0)) * 0.25) +
+ Math.floor((275 * mon) / 9.0) +
+ day +
+ 1721013.5 +
+ ((msec / 60000 + sec / 60.0 + minute) / 60.0 + hr) / 24.0 // ut in days
+ // # - 0.5*sgn(100.0*year + mon - 190002.5) + 0.5;
+ );
+}
+export function jday(yearOrDate, mon, day, hr, minute, sec, msec = 0) {
+ if (yearOrDate instanceof Date) {
+ const date = yearOrDate;
+ return jdayInternal(date.getUTCFullYear(), date.getUTCMonth() + 1, // Note, this function requires months in range 1-12.
+ date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds());
+ }
+ // biome-ignore lint/style/noNonNullAssertion: overloads make them non-nullable
+ return jdayInternal(yearOrDate, mon, day, hr, minute, sec, msec);
+}
+export function invjday(jd, asArray) {
+ // --------------- find year and days of the year -
+ const temp = jd - 2415019.5;
+ const tu = temp / 365.25;
+ let year = 1900 + Math.floor(tu);
+ let leapyrs = Math.floor((year - 1901) * 0.25);
+ // optional nudge by 8.64x10-7 sec to get even outputs
+ let days = temp - ((year - 1900) * 365.0 + leapyrs) + 0.00000000001;
+ // ------------ check for case of beginning of a year -----------
+ if (days < 1.0) {
+ year -= 1;
+ leapyrs = Math.floor((year - 1901) * 0.25);
+ days = temp - ((year - 1900) * 365.0 + leapyrs);
+ }
+ // ----------------- find remaing data -------------------------
+ const mdhms = days2mdhms(year, days);
+ const { mon, day, hr, minute } = mdhms;
+ const sec = mdhms.sec - 0.000000864;
+ if (asArray) {
+ return [year, mon, day, hr, minute, Math.floor(sec)];
+ }
+ return new Date(Date.UTC(year, mon - 1, day, hr, minute, Math.floor(sec)));
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/index.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/index.d.ts
new file mode 100644
index 0000000..a0e552a
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/index.d.ts
@@ -0,0 +1,13 @@
+/** biome-ignore-all assist/source/organizeImports: better order than alphabetical */
+export * as constants from './constants.js';
+export { jday, invjday } from './ext.js';
+export { twoline2satrec, json2satrec } from './io.js';
+export { propagate, type PropagateOptions, sgp4, gstime, } from './propagation.js';
+export { checkForDecay } from './propagation/check-for-decay.js';
+export { dopplerFactor } from './dopplerFactor.js';
+export { radiansToDegrees, degreesToRadians, degreesLat, degreesLong, radiansLat, radiansLong, geodeticToEcf, eciToGeodetic, eciToEcf, ecfToEci, ecfToLookAngles, } from './transforms.js';
+export { sunPos } from './sun.js';
+export * from './shadow.js';
+export { type SatRec, SatRecError } from './propagation/SatRec.js';
+export * from './common-types.js';
+export * from './wasm/index.js';
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/index.js b/Petabit/wwwroot/lib/satellite-js/dist/index.js
new file mode 100644
index 0000000..52d5e63
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/index.js
@@ -0,0 +1,13 @@
+/** biome-ignore-all assist/source/organizeImports: better order than alphabetical */
+export * as constants from './constants.js';
+export { jday, invjday } from './ext.js';
+export { twoline2satrec, json2satrec } from './io.js';
+export { propagate, sgp4, gstime, } from './propagation.js';
+export { checkForDecay } from './propagation/check-for-decay.js';
+export { dopplerFactor } from './dopplerFactor.js';
+export { radiansToDegrees, degreesToRadians, degreesLat, degreesLong, radiansLat, radiansLong, geodeticToEcf, eciToGeodetic, eciToEcf, ecfToEci, ecfToLookAngles, } from './transforms.js';
+export { sunPos } from './sun.js';
+export * from './shadow.js';
+export { SatRecError } from './propagation/SatRec.js';
+export * from './common-types.js';
+export * from './wasm/index.js';
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/io.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/io.d.ts
new file mode 100644
index 0000000..03f3ae6
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/io.d.ts
@@ -0,0 +1,19 @@
+import type { OMMJsonObject } from './common-types.js';
+import type { SatRec } from './propagation/SatRec.js';
+/**
+ * Return a Satellite imported from two lines of TLE data.
+ *
+ * Provide the two TLE lines as strings `tleLine1` and `tleLine2`,
+ * and select which standard set of gravitational constants you want
+ * by providing `gravity_constants`:
+ *
+ * `sgp4.propagation.wgs72` - Standard WGS 72 model
+ * `sgp4.propagation.wgs84` - More recent WGS 84 model
+ * `sgp4.propagation.wgs72old` - Legacy support for old SGP4 behavior
+ *
+ * Normally, computations are made using various recent improvements
+ * to the algorithm. If you want to turn some of these off and go
+ * back into "afspc" mode, then set `afspc_mode` to `True`.
+ */
+export declare function twoline2satrec(longstr1: string, longstr2: string): SatRec;
+export declare function json2satrec(jsonobj: OMMJsonObject, opsmode?: 'a' | 'i'): SatRec;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/io.js b/Petabit/wwwroot/lib/satellite-js/dist/io.js
new file mode 100644
index 0000000..3993595
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/io.js
@@ -0,0 +1,213 @@
+import { deg2rad, xpdotp } from './constants.js';
+import { days2mdhms, jday } from './ext.js';
+import { sgp4init } from './propagation/sgp4init.js';
+function initSatrec(satrec, opsmode) {
+ sgp4init(satrec, {
+ opsmode,
+ satn: satrec.satnum,
+ epoch: satrec.jdsatepoch - 2433281.5,
+ xbstar: satrec.bstar,
+ xecco: satrec.ecco,
+ xargpo: satrec.argpo,
+ xinclo: satrec.inclo,
+ xmo: satrec.mo,
+ xno: satrec.no,
+ xnodeo: satrec.nodeo,
+ });
+}
+/* -----------------------------------------------------------------------------
+ *
+ * function twoline2satrec
+ *
+ * this function converts the two line element set character string data to
+ * variables and initializes the sgp4 variables. several intermediate varaibles
+ * and quantities are determined. note that the result is a structure so multiple
+ * satellites can be processed simultaneously without having to reinitialize. the
+ * verification mode is an important option that permits quick checks of any
+ * changes to the underlying technical theory. this option works using a
+ * modified tle file in which the start, stop, and delta time values are
+ * included at the end of the second line of data. this only works with the
+ * verification mode. the catalog mode simply propagates from -1440 to 1440 min
+ * from epoch and is useful when performing entire catalog runs.
+ *
+ * author : david vallado 719-573-2600 1 mar 2001
+ *
+ * inputs :
+ * longstr1 - first line of the tle
+ * longstr2 - second line of the tle
+ * typerun - type of run verification 'v', catalog 'c',
+ * manual 'm'
+ * typeinput - type of manual input mfe 'm', epoch 'e', dayofyr 'd'
+ * opsmode - mode of operation afspc or improved 'a', 'i'
+ * whichconst - which set of constants to use 72, 84
+ *
+ * outputs :
+ * satrec - structure containing all the sgp4 satellite information
+ *
+ * coupling :
+ * getgravconst-
+ * days2mdhms - conversion of days to month, day, hour, minute, second
+ * jday - convert day month year hour minute second into julian date
+ * sgp4init - initialize the sgp4 variables
+ *
+ * references :
+ * norad spacetrack report #3
+ * vallado, crawford, hujsak, kelso 2006
+ --------------------------------------------------------------------------- */
+/**
+ * Return a Satellite imported from two lines of TLE data.
+ *
+ * Provide the two TLE lines as strings `tleLine1` and `tleLine2`,
+ * and select which standard set of gravitational constants you want
+ * by providing `gravity_constants`:
+ *
+ * `sgp4.propagation.wgs72` - Standard WGS 72 model
+ * `sgp4.propagation.wgs84` - More recent WGS 84 model
+ * `sgp4.propagation.wgs72old` - Legacy support for old SGP4 behavior
+ *
+ * Normally, computations are made using various recent improvements
+ * to the algorithm. If you want to turn some of these off and go
+ * back into "afspc" mode, then set `afspc_mode` to `True`.
+ */
+export function twoline2satrec(longstr1, longstr2) {
+ const opsmode = 'i';
+ const error = 0;
+ const satnum = longstr1.substring(2, 7);
+ const epochyr = parseInt(longstr1.substring(18, 20), 10);
+ const epochdays = parseFloat(longstr1.substring(20, 32));
+ let ndot = parseFloat(longstr1.substring(33, 43));
+ let nddot = parseFloat(`${longstr1.substring(44, 45)}.${longstr1.substring(45, 50)}E${longstr1.substring(50, 52)}`);
+ const bstar = parseFloat(`${longstr1.substring(53, 54)}.${longstr1.substring(54, 59)}E${longstr1.substring(59, 61)}`);
+ // satrec.satnum = longstr2.substring(2, 7);
+ // ---- find standard orbital elements ----
+ const inclo = parseFloat(longstr2.substring(8, 16)) * deg2rad;
+ const nodeo = parseFloat(longstr2.substring(17, 25)) * deg2rad;
+ const ecco = parseFloat(`.${longstr2.substring(26, 33).replace(/\s/g, '0')}`);
+ const argpo = parseFloat(longstr2.substring(34, 42)) * deg2rad;
+ const mo = parseFloat(longstr2.substring(43, 51)) * deg2rad;
+ // ---- find no, ndot, nddot ----
+ const no = parseFloat(longstr2.substring(52, 63)) / xpdotp;
+ // satrec.nddot= satrec.nddot * Math.pow(10.0, nexp);
+ // satrec.bstar= satrec.bstar * Math.pow(10.0, ibexp);
+ // ---- convert to sgp4 units ----
+ ndot /= xpdotp * 1440.0; // ? * minperday
+ nddot /= xpdotp * 1440.0 * 1440;
+ // ----------------------------------------------------------------
+ // find sgp4epoch time of element set
+ // remember that sgp4 uses units of days from 0 jan 1950 (sgp4epoch)
+ // and minutes from the epoch (time)
+ // ----------------------------------------------------------------
+ // ---------------- temp fix for years from 1957-2056 -------------------
+ // --------- correct fix will occur when year is 4-digit in tle ---------
+ const year = epochyr < 57 ? epochyr + 2000 : epochyr + 1900;
+ const mdhmsResult = days2mdhms(year, epochdays);
+ const { mon, day, hr, minute, sec } = mdhmsResult;
+ const jdsatepoch = jday(year, mon, day, hr, minute, sec);
+ const satrec = {
+ error,
+ satnum,
+ epochyr,
+ epochdays,
+ ndot,
+ nddot,
+ bstar,
+ inclo,
+ nodeo,
+ ecco,
+ argpo,
+ mo,
+ no,
+ jdsatepoch,
+ };
+ // ---------------- initialize SGP4 model -------------------
+ initSatrec(satrec, opsmode);
+ return satrec;
+}
+/* -----------------------------------------------------------------------------
+ *
+ * function json2satrec
+ *
+ * this function converts the OMM json data to variables and initializes the sgp4
+ * variables. several intermediate varaibles and quantities are determined. note
+ * that the result is a structure so multiple satellites can be processed
+ * simultaneously without having to reinitialize. the verification mode is an
+ * important option that permits quick checks of any changes to the underlying
+ * technical theory. this option works using a modified tle file in which the
+ * start, stop, and delta time values are included at the end of the second line
+ * of data. this only works with the verification mode. the catalog mode simply
+ * propagates from -1440 to 1440 min from epoch and is useful when performing
+ * entire catalog runs.
+ *
+ * author : Hariharan Vitaladevuni 18 Aug 2023
+ * Theodore Kruczek 19 Aug 2023
+ *
+ * inputs :
+ * jsonobj - OMM json data
+ * opsmode - mode of operation afspc or improved 'a', 'i'. Default: 'i'.
+ *
+ * outputs :
+ * satrec - structure containing all the sgp4 satellite information
+ *
+ * coupling :
+ * days2mdhms - conversion of days to month, day, hour, minute, second
+ * jday - convert day month year hour minute second into julian date
+ * sgp4init - initialize the sgp4 variables
+ *
+ * warning : the epoch date in OMM format is more accurate than TLE format!
+ * this will result in extremely close, but different
+ * position/velocity values. Depending on your use case, it may
+ * be better to use twoline2satrec, but for the average user this
+ * will provide comparable results.
+ *
+ * references :
+ * https://celestrak.org/NORAD/documentation/gp-data-formats.php
+ --------------------------------------------------------------------------- */
+export function json2satrec(jsonobj, opsmode = 'i') {
+ const error = 0;
+ const satnum = jsonobj.NORAD_CAT_ID.toString();
+ const epoch = new Date(jsonobj.EPOCH.endsWith('Z') ? jsonobj.EPOCH : `${jsonobj.EPOCH}Z`);
+ const year = epoch.getUTCFullYear();
+ const epochyr = Number(year.toString().slice(-2));
+ const epochdays = (epoch.valueOf() - new Date(Date.UTC(year, 0, 1, 0, 0, 0)).valueOf()) /
+ (86400 * 1000) +
+ 1;
+ let ndot = Number(jsonobj.MEAN_MOTION_DOT);
+ let nddot = Number(jsonobj.MEAN_MOTION_DDOT);
+ // ---- convert to sgp4 units ----
+ ndot /= xpdotp * 1440.0; // ? * minperday
+ nddot /= xpdotp * 1440.0 * 1440;
+ const bstar = Number(jsonobj.BSTAR);
+ const inclo = Number(jsonobj.INCLINATION) * deg2rad;
+ const nodeo = Number(jsonobj.RA_OF_ASC_NODE) * deg2rad;
+ const ecco = Number(jsonobj.ECCENTRICITY);
+ const argpo = Number(jsonobj.ARG_OF_PERICENTER) * deg2rad;
+ const mo = Number(jsonobj.MEAN_ANOMALY) * deg2rad;
+ const no = Number(jsonobj.MEAN_MOTION) / xpdotp;
+ // ----------------------------------------------------------------
+ // find sgp4epoch time of element set
+ // remember that sgp4 uses units of days from 0 jan 1950 (sgp4epoch)
+ // and minutes from the epoch (time)
+ // ----------------------------------------------------------------
+ const mdhmsResult = days2mdhms(year, epochdays);
+ const { mon, day, hr, minute, sec } = mdhmsResult;
+ const jdsatepoch = jday(year, mon, day, hr, minute, sec);
+ const satrec = {
+ error,
+ satnum,
+ epochyr,
+ epochdays,
+ ndot,
+ nddot,
+ bstar,
+ inclo,
+ nodeo,
+ ecco,
+ argpo,
+ mo,
+ no,
+ jdsatepoch,
+ };
+ // ---------------- initialize SGP4 model -------------------
+ initSatrec(satrec, opsmode);
+ return satrec;
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation.d.ts
new file mode 100644
index 0000000..23222ff
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation.d.ts
@@ -0,0 +1,3 @@
+export { gstime } from './propagation/gstime.js';
+export { type PropagateOptions, propagate } from './propagation/propagate.js';
+export { sgp4 } from './propagation/sgp4.js';
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation.js
new file mode 100644
index 0000000..2afd287
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation.js
@@ -0,0 +1,3 @@
+export { gstime } from './propagation/gstime.js';
+export { propagate } from './propagation/propagate.js';
+export { sgp4 } from './propagation/sgp4.js';
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/SatRec.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/SatRec.d.ts
new file mode 100644
index 0000000..5dd1526
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/SatRec.d.ts
@@ -0,0 +1,205 @@
+export declare enum SatRecError {
+ /**
+ * No error, propagation for the last supplied date is successful
+ */
+ None = 0,
+ /**
+ * Mean eccentricity is out of range 0 ≤ e < 1
+ */
+ MeanEccentricityOutOfRange = 1,
+ /**
+ * Mean motion has fallen below zero.
+ */
+ MeanMotionBelowZero = 2,
+ /**
+ * Perturbed eccentricity is out of range 0 ≤ e < 1
+ */
+ PerturbedEccentricityOutOfRange = 3,
+ /**
+ * Length of the orbit’s semi-latus rectum has fallen below zero.
+ */
+ SemiLatusRectumBelowZero = 4,
+ /**
+ * Orbit has decayed: the computed position is underground.
+ */
+ Decayed = 6
+}
+/**
+ * A structure that contains all the information needed to propagate
+ * a satellite's orbit using the SGP4 model.
+ *
+ * Mostly you can consider it opaque as you only need to pass it to `propagate` function.
+ * All properties should be considered read-only as they're used and set by SGP4 model internally.
+ *
+ * This interface is a direct translation of C++ struct `elsetrec`
+ * from the source code by David Vallado; all changes to the original struct are documented.
+ */
+export interface SatRec {
+ /**
+ * Stringified satellite number, usually from NORAD catalog
+ */
+ satnum: string;
+ /**
+ * Full four-digit year of this element set's epoch moment
+ */
+ epochyr: number;
+ epochtynumrev: number;
+ /**
+ * Error code produced by the most recent SGP4 propagation you performed with this element set.
+ */
+ error: SatRecError;
+ /**
+ * A single character that directs SGP4 to either operate in its modern 'i' improved mode or
+ * in its legacy 'a' AFSPC mode.
+ */
+ operationmode: 'a' | 'i';
+ init: 'y' | 'n';
+ /**
+ * A single character, chosen automatically when the orbital elements were loaded, that
+ * indicates whether SGP4 has chosen to use its built-in 'n' Near Earth or 'd' Deep Space
+ * mode for this satellite.
+ */
+ method: 'n' | 'd';
+ isimp: number;
+ aycof: number;
+ con41: number;
+ cc1: number;
+ cc4: number;
+ cc5: number;
+ d2: number;
+ d3: number;
+ d4: number;
+ delmo: number;
+ eta: number;
+ argpdot: number;
+ omgcof: number;
+ sinmao: number;
+ /**
+ * The time you gave when you most recently asked SGP4 to compute this satellite’s position,
+ * measured in minutes before (negative) or after (positive) the satellite’s epoch.
+ */
+ t: number;
+ t2cof: number;
+ t3cof: number;
+ t4cof: number;
+ t5cof: number;
+ x1mth2: number;
+ x7thm1: number;
+ mdot: number;
+ nodedot: number;
+ xlcof: number;
+ xmcof: number;
+ nodecf: number;
+ irez: number;
+ d2201: number;
+ d2211: number;
+ d3210: number;
+ d3222: number;
+ d4410: number;
+ d4422: number;
+ d5220: number;
+ d5232: number;
+ d5421: number;
+ d5433: number;
+ dedt: number;
+ del1: number;
+ del2: number;
+ del3: number;
+ didt: number;
+ dmdt: number;
+ dnodt: number;
+ domdt: number;
+ e3: number;
+ ee2: number;
+ peo: number;
+ pgho: number;
+ pho: number;
+ pinco: number;
+ plo: number;
+ se2: number;
+ se3: number;
+ sgh2: number;
+ sgh3: number;
+ sgh4: number;
+ sh2: number;
+ sh3: number;
+ si2: number;
+ si3: number;
+ sl2: number;
+ sl3: number;
+ sl4: number;
+ gsto: number;
+ xfact: number;
+ xgh2: number;
+ xgh3: number;
+ xgh4: number;
+ xh2: number;
+ xh3: number;
+ xi2: number;
+ xi3: number;
+ xl2: number;
+ xl3: number;
+ xl4: number;
+ xlamo: number;
+ zmol: number;
+ zmos: number;
+ atime: number;
+ xli: number;
+ xni: number;
+ a: number;
+ altp: number;
+ alta: number;
+ /**
+ * Fractional days into the year of the epoch moment in UTC.
+ */
+ epochdays: number;
+ /**
+ * Julian date of the epoch (computed from epochyr and epochdays).
+ */
+ jdsatepoch: number;
+ /**
+ * Second time derivative of the mean motion, in radians/minute^3 (ignored by SGP4).
+ */
+ nddot: number;
+ /**
+ * First time derivative of the mean motion, in radians/minute^2 (ignored by SGP4).
+ */
+ ndot: number;
+ /**
+ * Ballistic drag coefficient B* in inverse earth radii.
+ */
+ bstar: number;
+ /**
+ * Inclination in radians.
+ */
+ inclo: number;
+ /**
+ * Right ascension of ascending node in radians.
+ */
+ nodeo: number;
+ /**
+ * Eccentricity.
+ */
+ ecco: number;
+ /**
+ * Argument of perigee in radians.
+ */
+ argpo: number;
+ /**
+ * Mean anomaly in radians.
+ */
+ mo: number;
+ /**
+ * Mean motion in radians per minute without kozai.
+ */
+ no: number;
+ /**
+ * Mean motion in radians per minute with kozai.
+ */
+ nokozai: number;
+ /**
+ * sgp4 algorithm internal variable that allows to filter out satellites decayed long ago
+ */
+ tempa: number;
+}
+export type SatRecInit = Pick;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/SatRec.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/SatRec.js
new file mode 100644
index 0000000..c2ec213
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/SatRec.js
@@ -0,0 +1,28 @@
+export var SatRecError;
+(function (SatRecError) {
+ /**
+ * No error, propagation for the last supplied date is successful
+ */
+ SatRecError[SatRecError["None"] = 0] = "None";
+ /**
+ * Mean eccentricity is out of range 0 ≤ e < 1
+ */
+ SatRecError[SatRecError["MeanEccentricityOutOfRange"] = 1] = "MeanEccentricityOutOfRange";
+ /**
+ * Mean motion has fallen below zero.
+ */
+ SatRecError[SatRecError["MeanMotionBelowZero"] = 2] = "MeanMotionBelowZero";
+ /**
+ * Perturbed eccentricity is out of range 0 ≤ e < 1
+ */
+ SatRecError[SatRecError["PerturbedEccentricityOutOfRange"] = 3] = "PerturbedEccentricityOutOfRange";
+ /**
+ * Length of the orbit’s semi-latus rectum has fallen below zero.
+ */
+ SatRecError[SatRecError["SemiLatusRectumBelowZero"] = 4] = "SemiLatusRectumBelowZero";
+ // 5 is not used
+ /**
+ * Orbit has decayed: the computed position is underground.
+ */
+ SatRecError[SatRecError["Decayed"] = 6] = "Decayed";
+})(SatRecError || (SatRecError = {}));
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/check-for-decay.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/check-for-decay.d.ts
new file mode 100644
index 0000000..1be7e0a
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/check-for-decay.d.ts
@@ -0,0 +1,28 @@
+import type { SatRec } from './SatRec.js';
+/**
+ * In some cases, usually for objects decayed long time ago,
+ * SGP4 would return garbage position and speed data instead of indicating that
+ * the satellite has actually decayed. This check indicates if it's one of those cases.
+ *
+ * Note: this community check DOES NOT EXIST in the original SGP4 algorithm, so your results
+ * MAY DIFFER from the results of official SGP4 propagation when using this check.
+ *
+ * @returns `true` if the satellite is authoritatively decayed as a result of latest propagation call.
+ * If the latest propagation call gave a non-null result, that result should be discarded.
+ *
+ * @example
+ * ```ts
+ * const result = sgp4(satrec, 0);
+ * if (result && checkForDecay(satrec)) {
+ * // here SGP4 model reported successful propagation,
+ * //but the check caught that the satellite has decayed.
+ * }
+ * ```
+ *
+ * @example
+ * ```ts
+ * const result = propagate(satrec, new Date(), { communityDecayCheckEnabled: true })
+ * // result is non-null only if `checkForDecay` too indicates that the satellite HAS NOT decayed.
+ * ```
+ */
+export declare const checkForDecay: (satrec: SatRec) => boolean;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/check-for-decay.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/check-for-decay.js
new file mode 100644
index 0000000..dea943c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/check-for-decay.js
@@ -0,0 +1,30 @@
+// This check is authored by Theodore Kruczek for his library OOTK:
+// https://github.com/thkruz/ootk
+// Used with permission: https://github.com/thkruz/ootk/issues/31#issuecomment-4893495872
+/**
+ * In some cases, usually for objects decayed long time ago,
+ * SGP4 would return garbage position and speed data instead of indicating that
+ * the satellite has actually decayed. This check indicates if it's one of those cases.
+ *
+ * Note: this community check DOES NOT EXIST in the original SGP4 algorithm, so your results
+ * MAY DIFFER from the results of official SGP4 propagation when using this check.
+ *
+ * @returns `true` if the satellite is authoritatively decayed as a result of latest propagation call.
+ * If the latest propagation call gave a non-null result, that result should be discarded.
+ *
+ * @example
+ * ```ts
+ * const result = sgp4(satrec, 0);
+ * if (result && checkForDecay(satrec)) {
+ * // here SGP4 model reported successful propagation,
+ * //but the check caught that the satellite has decayed.
+ * }
+ * ```
+ *
+ * @example
+ * ```ts
+ * const result = propagate(satrec, new Date(), { communityDecayCheckEnabled: true })
+ * // result is non-null only if `checkForDecay` too indicates that the satellite HAS NOT decayed.
+ * ```
+ */
+export const checkForDecay = (satrec) => satrec.tempa <= 0;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/dpper.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dpper.d.ts
new file mode 100644
index 0000000..ecd80e6
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dpper.d.ts
@@ -0,0 +1,18 @@
+import type { SatRec } from './SatRec.js';
+interface DpperOptions {
+ init: 'y' | 'n';
+ opsmode: 'a' | 'i';
+ ep: number;
+ inclp: number;
+ nodep: number;
+ argpp: number;
+ mp: number;
+}
+export declare function dpper(satrec: SatRec, options: DpperOptions): {
+ ep: number;
+ inclp: number;
+ nodep: number;
+ argpp: number;
+ mp: number;
+};
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/dpper.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dpper.js
new file mode 100644
index 0000000..83e190e
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dpper.js
@@ -0,0 +1,204 @@
+import { pi, twoPi } from '../constants.js';
+/* -----------------------------------------------------------------------------
+ *
+ * procedure dpper
+ *
+ * this procedure provides deep space long period periodic contributions
+ * to the mean elements. by design, these periodics are zero at epoch.
+ * this used to be dscom which included initialization, but it's really a
+ * recurring function.
+ *
+ * author : david vallado 719-573-2600 28 jun 2005
+ *
+ * inputs :
+ * e3 -
+ * ee2 -
+ * peo -
+ * pgho -
+ * pho -
+ * pinco -
+ * plo -
+ * se2 , se3 , sgh2, sgh3, sgh4, sh2, sh3, si2, si3, sl2, sl3, sl4 -
+ * t -
+ * xh2, xh3, xi2, xi3, xl2, xl3, xl4 -
+ * zmol -
+ * zmos -
+ * ep - eccentricity 0.0 - 1.0
+ * inclo - inclination - needed for lyddane modification
+ * nodep - right ascension of ascending node
+ * argpp - argument of perigee
+ * mp - mean anomaly
+ *
+ * outputs :
+ * ep - eccentricity 0.0 - 1.0
+ * inclp - inclination
+ * nodep - right ascension of ascending node
+ * argpp - argument of perigee
+ * mp - mean anomaly
+ *
+ * locals :
+ * alfdp -
+ * betdp -
+ * cosip , sinip , cosop , sinop ,
+ * dalf -
+ * dbet -
+ * dls -
+ * f2, f3 -
+ * pe -
+ * pgh -
+ * ph -
+ * pinc -
+ * pl -
+ * sel , ses , sghl , sghs , shl , shs , sil , sinzf , sis ,
+ * sll , sls
+ * xls -
+ * xnoh -
+ * zf -
+ * zm -
+ *
+ * coupling :
+ * none.
+ *
+ * references :
+ * hoots, roehrich, norad spacetrack report #3 1980
+ * hoots, norad spacetrack report #6 1986
+ * hoots, schumacher and glover 2004
+ * vallado, crawford, hujsak, kelso 2006
+ ----------------------------------------------------------------------------*/
+export function dpper(satrec, options) {
+ const { e3, ee2, peo, pgho, pho, pinco, plo, se2, se3, sgh2, sgh3, sgh4, sh2, sh3, si2, si3, sl2, sl3, sl4, t, xgh2, xgh3, xgh4, xh2, xh3, xi2, xi3, xl2, xl3, xl4, zmol, zmos, } = satrec;
+ const { init, opsmode } = options;
+ let { ep, inclp, nodep, argpp, mp } = options;
+ // Copy satellite attributes into local variables for convenience
+ // and symmetry in writing formulae.
+ let alfdp;
+ let betdp;
+ let cosip;
+ let sinip;
+ let cosop;
+ let sinop;
+ let dalf;
+ let dbet;
+ let dls;
+ let f2;
+ let f3;
+ let pe;
+ let pgh;
+ let ph;
+ let pinc;
+ let pl;
+ let sinzf;
+ let xls;
+ let xnoh;
+ let zf;
+ let zm;
+ // ---------------------- constants -----------------------------
+ const zns = 1.19459e-5;
+ const zes = 0.01675;
+ const znl = 1.5835218e-4;
+ const zel = 0.0549;
+ // --------------- calculate time varying periodics -----------
+ zm = zmos + zns * t;
+ // be sure that the initial call has time set to zero
+ if (init === 'y') {
+ zm = zmos;
+ }
+ zf = zm + 2.0 * zes * Math.sin(zm);
+ sinzf = Math.sin(zf);
+ f2 = 0.5 * sinzf * sinzf - 0.25;
+ f3 = -0.5 * sinzf * Math.cos(zf);
+ const ses = se2 * f2 + se3 * f3;
+ const sis = si2 * f2 + si3 * f3;
+ const sls = sl2 * f2 + sl3 * f3 + sl4 * sinzf;
+ const sghs = sgh2 * f2 + sgh3 * f3 + sgh4 * sinzf;
+ const shs = sh2 * f2 + sh3 * f3;
+ zm = zmol + znl * t;
+ if (init === 'y') {
+ zm = zmol;
+ }
+ zf = zm + 2.0 * zel * Math.sin(zm);
+ sinzf = Math.sin(zf);
+ f2 = 0.5 * sinzf * sinzf - 0.25;
+ f3 = -0.5 * sinzf * Math.cos(zf);
+ const sel = ee2 * f2 + e3 * f3;
+ const sil = xi2 * f2 + xi3 * f3;
+ const sll = xl2 * f2 + xl3 * f3 + xl4 * sinzf;
+ const sghl = xgh2 * f2 + xgh3 * f3 + xgh4 * sinzf;
+ const shll = xh2 * f2 + xh3 * f3;
+ pe = ses + sel;
+ pinc = sis + sil;
+ pl = sls + sll;
+ pgh = sghs + sghl;
+ ph = shs + shll;
+ if (init === 'n') {
+ pe -= peo;
+ pinc -= pinco;
+ pl -= plo;
+ pgh -= pgho;
+ ph -= pho;
+ inclp += pinc;
+ ep += pe;
+ sinip = Math.sin(inclp);
+ cosip = Math.cos(inclp);
+ /* ----------------- apply periodics directly ------------ */
+ // sgp4fix for lyddane choice
+ // strn3 used original inclination - this is technically feasible
+ // gsfc used perturbed inclination - also technically feasible
+ // probably best to readjust the 0.2 limit value and limit discontinuity
+ // 0.2 rad = 11.45916 deg
+ // use next line for original strn3 approach and original inclination
+ // if (inclo >= 0.2)
+ // use next line for gsfc version and perturbed inclination
+ if (inclp >= 0.2) {
+ ph /= sinip;
+ pgh -= cosip * ph;
+ argpp += pgh;
+ nodep += ph;
+ mp += pl;
+ }
+ else {
+ // ---- apply periodics with lyddane modification ----
+ sinop = Math.sin(nodep);
+ cosop = Math.cos(nodep);
+ alfdp = sinip * sinop;
+ betdp = sinip * cosop;
+ dalf = ph * cosop + pinc * cosip * sinop;
+ dbet = -ph * sinop + pinc * cosip * cosop;
+ alfdp += dalf;
+ betdp += dbet;
+ nodep %= twoPi;
+ // sgp4fix for afspc written intrinsic functions
+ // nodep used without a trigonometric function ahead
+ if (nodep < 0.0 && opsmode === 'a') {
+ nodep += twoPi;
+ }
+ xls = mp + argpp + cosip * nodep;
+ dls = pl + pgh - pinc * nodep * sinip;
+ xls += dls;
+ xnoh = nodep;
+ nodep = Math.atan2(alfdp, betdp);
+ // sgp4fix for afspc written intrinsic functions
+ // nodep used without a trigonometric function ahead
+ if (nodep < 0.0 && opsmode === 'a') {
+ nodep += twoPi;
+ }
+ if (Math.abs(xnoh - nodep) > pi) {
+ if (nodep < xnoh) {
+ nodep += twoPi;
+ }
+ else {
+ nodep -= twoPi;
+ }
+ }
+ mp += pl;
+ argpp = xls - mp - cosip * nodep;
+ }
+ }
+ return {
+ ep,
+ inclp,
+ nodep,
+ argpp,
+ mp,
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/dscom.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dscom.d.ts
new file mode 100644
index 0000000..37db50e
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dscom.d.ts
@@ -0,0 +1,93 @@
+interface DscomOptions {
+ epoch: number;
+ ep: number;
+ argpp: number;
+ tc: number;
+ inclp: number;
+ nodep: number;
+ np: number;
+}
+export declare function dscom(options: DscomOptions): {
+ snodm: number;
+ cnodm: number;
+ sinim: number;
+ cosim: number;
+ sinomm: number;
+ cosomm: number;
+ day: number;
+ e3: number;
+ ee2: number;
+ em: number;
+ emsq: number;
+ gam: number;
+ peo: number;
+ pgho: number;
+ pho: number;
+ pinco: number;
+ plo: number;
+ rtemsq: number;
+ se2: number;
+ se3: number;
+ sgh2: number;
+ sgh3: number;
+ sgh4: number;
+ sh2: number;
+ sh3: number;
+ si2: number;
+ si3: number;
+ sl2: number;
+ sl3: number;
+ sl4: number;
+ s1: number;
+ s2: number;
+ s3: number;
+ s4: number;
+ s5: number;
+ s6: number;
+ s7: number;
+ ss1: number;
+ ss2: number;
+ ss3: number;
+ ss4: number;
+ ss5: number;
+ ss6: number;
+ ss7: number;
+ sz1: number;
+ sz2: number;
+ sz3: number;
+ sz11: number;
+ sz12: number;
+ sz13: number;
+ sz21: number;
+ sz22: number;
+ sz23: number;
+ sz31: number;
+ sz32: number;
+ sz33: number;
+ xgh2: number;
+ xgh3: number;
+ xgh4: number;
+ xh2: number;
+ xh3: number;
+ xi2: number;
+ xi3: number;
+ xl2: number;
+ xl3: number;
+ xl4: number;
+ nm: number;
+ z1: number;
+ z2: number;
+ z3: number;
+ z11: number;
+ z12: number;
+ z13: number;
+ z21: number;
+ z22: number;
+ z23: number;
+ z31: number;
+ z32: number;
+ z33: number;
+ zmol: number;
+ zmos: number;
+};
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/dscom.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dscom.js
new file mode 100644
index 0000000..1f1ed2c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dscom.js
@@ -0,0 +1,378 @@
+/** biome-ignore-all lint/style/noNonNullAssertion: due to the fact that
+ * many variables are first assigned within loops and if statements,
+ * Typescript marks many of them as "used before assigned". To keep the algorithm
+ * as is, non null assertion is used there.
+ */
+import { twoPi } from '../constants.js';
+/*-----------------------------------------------------------------------------
+ *
+ * procedure dscom
+ *
+ * this procedure provides deep space common items used by both the secular
+ * and periodics subroutines. input is provided as shown. this routine
+ * used to be called dpper, but the functions inside weren't well organized.
+ *
+ * author : david vallado 719-573-2600 28 jun 2005
+ *
+ * inputs :
+ * epoch -
+ * ep - eccentricity
+ * argpp - argument of perigee
+ * tc -
+ * inclp - inclination
+ * nodep - right ascension of ascending node
+ * np - mean motion
+ *
+ * outputs :
+ * sinim , cosim , sinomm , cosomm , snodm , cnodm
+ * day -
+ * e3 -
+ * ee2 -
+ * em - eccentricity
+ * emsq - eccentricity squared
+ * gam -
+ * peo -
+ * pgho -
+ * pho -
+ * pinco -
+ * plo -
+ * rtemsq -
+ * se2, se3 -
+ * sgh2, sgh3, sgh4 -
+ * sh2, sh3, si2, si3, sl2, sl3, sl4 -
+ * s1, s2, s3, s4, s5, s6, s7 -
+ * ss1, ss2, ss3, ss4, ss5, ss6, ss7, sz1, sz2, sz3 -
+ * sz11, sz12, sz13, sz21, sz22, sz23, sz31, sz32, sz33 -
+ * xgh2, xgh3, xgh4, xh2, xh3, xi2, xi3, xl2, xl3, xl4 -
+ * nm - mean motion
+ * z1, z2, z3, z11, z12, z13, z21, z22, z23, z31, z32, z33 -
+ * zmol -
+ * zmos -
+ *
+ * locals :
+ * a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 -
+ * betasq -
+ * cc -
+ * ctem, stem -
+ * x1, x2, x3, x4, x5, x6, x7, x8 -
+ * xnodce -
+ * xnoi -
+ * zcosg , zsing , zcosgl , zsingl , zcosh , zsinh , zcoshl , zsinhl ,
+ * zcosi , zsini , zcosil , zsinil ,
+ * zx -
+ * zy -
+ *
+ * coupling :
+ * none.
+ *
+ * references :
+ * hoots, roehrich, norad spacetrack report #3 1980
+ * hoots, norad spacetrack report #6 1986
+ * hoots, schumacher and glover 2004
+ * vallado, crawford, hujsak, kelso 2006
+ ----------------------------------------------------------------------------*/
+export function dscom(options) {
+ const { epoch, ep, argpp, tc, inclp, nodep, np } = options;
+ let a1;
+ let a2;
+ let a3;
+ let a4;
+ let a5;
+ let a6;
+ let a7;
+ let a8;
+ let a9;
+ let a10;
+ let cc;
+ let x1;
+ let x2;
+ let x3;
+ let x4;
+ let x5;
+ let x6;
+ let x7;
+ let x8;
+ let zcosg;
+ let zsing;
+ let zcosh;
+ let zsinh;
+ let zcosi;
+ let zsini;
+ let ss1;
+ let ss2;
+ let ss3;
+ let ss4;
+ let ss5;
+ let ss6;
+ let ss7;
+ let sz1;
+ let sz2;
+ let sz3;
+ let sz11;
+ let sz12;
+ let sz13;
+ let sz21;
+ let sz22;
+ let sz23;
+ let sz31;
+ let sz32;
+ let sz33;
+ let s1;
+ let s2;
+ let s3;
+ let s4;
+ let s5;
+ let s6;
+ let s7;
+ let z1;
+ let z2;
+ let z3;
+ let z11;
+ let z12;
+ let z13;
+ let z21;
+ let z22;
+ let z23;
+ let z31;
+ let z32;
+ let z33;
+ // -------------------------- constants -------------------------
+ const zes = 0.01675;
+ const zel = 0.0549;
+ const c1ss = 2.9864797e-6;
+ const c1l = 4.7968065e-7;
+ const zsinis = 0.39785416;
+ const zcosis = 0.91744867;
+ const zcosgs = 0.1945905;
+ const zsings = -0.98088458;
+ // --------------------- local variables ------------------------
+ const nm = np;
+ const em = ep;
+ const snodm = Math.sin(nodep);
+ const cnodm = Math.cos(nodep);
+ const sinomm = Math.sin(argpp);
+ const cosomm = Math.cos(argpp);
+ const sinim = Math.sin(inclp);
+ const cosim = Math.cos(inclp);
+ const emsq = em * em;
+ const betasq = 1.0 - emsq;
+ const rtemsq = Math.sqrt(betasq);
+ // ----------------- initialize lunar solar terms ---------------
+ const peo = 0.0;
+ const pinco = 0.0;
+ const plo = 0.0;
+ const pgho = 0.0;
+ const pho = 0.0;
+ const day = epoch + 18261.5 + tc / 1440.0;
+ const xnodce = (4.523602 - 9.2422029e-4 * day) % twoPi;
+ const stem = Math.sin(xnodce);
+ const ctem = Math.cos(xnodce);
+ const zcosil = 0.91375164 - 0.03568096 * ctem;
+ const zsinil = Math.sqrt(1.0 - zcosil * zcosil);
+ const zsinhl = (0.089683511 * stem) / zsinil;
+ const zcoshl = Math.sqrt(1.0 - zsinhl * zsinhl);
+ const gam = 5.8351514 + 0.001944368 * day;
+ let zx = (0.39785416 * stem) / zsinil;
+ const zy = zcoshl * ctem + 0.91744867 * zsinhl * stem;
+ zx = Math.atan2(zx, zy);
+ zx += gam - xnodce;
+ const zcosgl = Math.cos(zx);
+ const zsingl = Math.sin(zx);
+ // ------------------------- do solar terms ---------------------
+ zcosg = zcosgs;
+ zsing = zsings;
+ zcosi = zcosis;
+ zsini = zsinis;
+ zcosh = cnodm;
+ zsinh = snodm;
+ cc = c1ss;
+ const xnoi = 1.0 / nm;
+ let lsflg = 0;
+ while (lsflg < 2) {
+ lsflg += 1;
+ a1 = zcosg * zcosh + zsing * zcosi * zsinh;
+ a3 = -zsing * zcosh + zcosg * zcosi * zsinh;
+ a7 = -zcosg * zsinh + zsing * zcosi * zcosh;
+ a8 = zsing * zsini;
+ a9 = zsing * zsinh + zcosg * zcosi * zcosh;
+ a10 = zcosg * zsini;
+ a2 = cosim * a7 + sinim * a8;
+ a4 = cosim * a9 + sinim * a10;
+ a5 = -sinim * a7 + cosim * a8;
+ a6 = -sinim * a9 + cosim * a10;
+ x1 = a1 * cosomm + a2 * sinomm;
+ x2 = a3 * cosomm + a4 * sinomm;
+ x3 = -a1 * sinomm + a2 * cosomm;
+ x4 = -a3 * sinomm + a4 * cosomm;
+ x5 = a5 * sinomm;
+ x6 = a6 * sinomm;
+ x7 = a5 * cosomm;
+ x8 = a6 * cosomm;
+ z31 = 12.0 * x1 * x1 - 3.0 * x3 * x3;
+ z32 = 24.0 * x1 * x2 - 6.0 * x3 * x4;
+ z33 = 12.0 * x2 * x2 - 3.0 * x4 * x4;
+ z1 = 3.0 * (a1 * a1 + a2 * a2) + z31 * emsq;
+ z2 = 6.0 * (a1 * a3 + a2 * a4) + z32 * emsq;
+ z3 = 3.0 * (a3 * a3 + a4 * a4) + z33 * emsq;
+ z11 = -6.0 * a1 * a5 + emsq * (-24.0 * x1 * x7 - 6.0 * x3 * x5);
+ z12 =
+ -6.0 * (a1 * a6 + a3 * a5) +
+ emsq * (-24.0 * (x2 * x7 + x1 * x8) + -6.0 * (x3 * x6 + x4 * x5));
+ z13 = -6.0 * a3 * a6 + emsq * (-24.0 * x2 * x8 - 6.0 * x4 * x6);
+ z21 = 6.0 * a2 * a5 + emsq * (24.0 * x1 * x5 - 6.0 * x3 * x7);
+ z22 =
+ 6.0 * (a4 * a5 + a2 * a6) +
+ emsq * (24.0 * (x2 * x5 + x1 * x6) - 6.0 * (x4 * x7 + x3 * x8));
+ z23 = 6.0 * a4 * a6 + emsq * (24.0 * x2 * x6 - 6.0 * x4 * x8);
+ z1 = z1 + z1 + betasq * z31;
+ z2 = z2 + z2 + betasq * z32;
+ z3 = z3 + z3 + betasq * z33;
+ s3 = cc * xnoi;
+ s2 = (-0.5 * s3) / rtemsq;
+ s4 = s3 * rtemsq;
+ s1 = -15.0 * em * s4;
+ s5 = x1 * x3 + x2 * x4;
+ s6 = x2 * x3 + x1 * x4;
+ s7 = x2 * x4 - x1 * x3;
+ // ----------------------- do lunar terms -------------------
+ if (lsflg === 1) {
+ ss1 = s1;
+ ss2 = s2;
+ ss3 = s3;
+ ss4 = s4;
+ ss5 = s5;
+ ss6 = s6;
+ ss7 = s7;
+ sz1 = z1;
+ sz2 = z2;
+ sz3 = z3;
+ sz11 = z11;
+ sz12 = z12;
+ sz13 = z13;
+ sz21 = z21;
+ sz22 = z22;
+ sz23 = z23;
+ sz31 = z31;
+ sz32 = z32;
+ sz33 = z33;
+ zcosg = zcosgl;
+ zsing = zsingl;
+ zcosi = zcosil;
+ zsini = zsinil;
+ zcosh = zcoshl * cnodm + zsinhl * snodm;
+ zsinh = snodm * zcoshl - cnodm * zsinhl;
+ cc = c1l;
+ }
+ }
+ const zmol = (4.7199672 + (0.2299715 * day - gam)) % twoPi;
+ const zmos = (6.2565837 + 0.017201977 * day) % twoPi;
+ // ------------------------ do solar terms ----------------------
+ const se2 = 2.0 * ss1 * ss6;
+ const se3 = 2.0 * ss1 * ss7;
+ const si2 = 2.0 * ss2 * sz12;
+ const si3 = 2.0 * ss2 * (sz13 - sz11);
+ const sl2 = -2.0 * ss3 * sz2;
+ const sl3 = -2.0 * ss3 * (sz3 - sz1);
+ const sl4 = -2.0 * ss3 * (-21.0 - 9.0 * emsq) * zes;
+ const sgh2 = 2.0 * ss4 * sz32;
+ const sgh3 = 2.0 * ss4 * (sz33 - sz31);
+ const sgh4 = -18.0 * ss4 * zes;
+ const sh2 = -2.0 * ss2 * sz22;
+ const sh3 = -2.0 * ss2 * (sz23 - sz21);
+ // ------------------------ do lunar terms ----------------------
+ const ee2 = 2.0 * s1 * s6;
+ const e3 = 2.0 * s1 * s7;
+ const xi2 = 2.0 * s2 * z12;
+ const xi3 = 2.0 * s2 * (z13 - z11);
+ const xl2 = -2.0 * s3 * z2;
+ const xl3 = -2.0 * s3 * (z3 - z1);
+ const xl4 = -2.0 * s3 * (-21.0 - 9.0 * emsq) * zel;
+ const xgh2 = 2.0 * s4 * z32;
+ const xgh3 = 2.0 * s4 * (z33 - z31);
+ const xgh4 = -18.0 * s4 * zel;
+ const xh2 = -2.0 * s2 * z22;
+ const xh3 = -2.0 * s2 * (z23 - z21);
+ return {
+ snodm,
+ cnodm,
+ sinim,
+ cosim,
+ sinomm,
+ cosomm,
+ day,
+ e3,
+ ee2,
+ em,
+ emsq,
+ gam,
+ peo,
+ pgho,
+ pho,
+ pinco,
+ plo,
+ rtemsq,
+ se2,
+ se3,
+ sgh2,
+ sgh3,
+ sgh4,
+ sh2,
+ sh3,
+ si2,
+ si3,
+ sl2,
+ sl3,
+ sl4,
+ s1: s1,
+ s2: s2,
+ s3: s3,
+ s4: s4,
+ s5: s5,
+ s6: s6,
+ s7: s7,
+ ss1: ss1,
+ ss2: ss2,
+ ss3: ss3,
+ ss4: ss4,
+ ss5: ss5,
+ ss6: ss6,
+ ss7: ss7,
+ sz1: sz1,
+ sz2: sz2,
+ sz3: sz3,
+ sz11: sz11,
+ sz12: sz12,
+ sz13: sz13,
+ sz21: sz21,
+ sz22: sz22,
+ sz23: sz23,
+ sz31: sz31,
+ sz32: sz32,
+ sz33: sz33,
+ xgh2,
+ xgh3,
+ xgh4,
+ xh2,
+ xh3,
+ xi2,
+ xi3,
+ xl2,
+ xl3,
+ xl4,
+ nm,
+ z1: z1,
+ z2: z2,
+ z3: z3,
+ z11: z11,
+ z12: z12,
+ z13: z13,
+ z21: z21,
+ z22: z22,
+ z23: z23,
+ z31: z31,
+ z32: z32,
+ z33: z33,
+ zmol,
+ zmos,
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/dsinit.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dsinit.d.ts
new file mode 100644
index 0000000..6017563
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dsinit.d.ts
@@ -0,0 +1,107 @@
+interface DsinitOptions {
+ cosim: number;
+ emsq: number;
+ argpo: number;
+ s1: number;
+ s2: number;
+ s3: number;
+ s4: number;
+ s5: number;
+ sinim: number;
+ ss1: number;
+ ss2: number;
+ ss3: number;
+ ss4: number;
+ ss5: number;
+ sz1: number;
+ sz3: number;
+ sz11: number;
+ sz13: number;
+ sz21: number;
+ sz23: number;
+ sz31: number;
+ sz33: number;
+ t: number;
+ tc: number;
+ gsto: number;
+ mo: number;
+ mdot: number;
+ no: number;
+ nodeo: number;
+ nodedot: number;
+ xpidot: number;
+ z1: number;
+ z3: number;
+ z11: number;
+ z13: number;
+ z21: number;
+ z23: number;
+ z31: number;
+ z33: number;
+ ecco: number;
+ eccsq: number;
+ em: number;
+ argpm: number;
+ inclm: number;
+ mm: number;
+ nm: number;
+ nodem: number;
+ irez: number;
+ atime: number;
+ d2201: number;
+ d2211: number;
+ d3210: number;
+ d3222: number;
+ d4410: number;
+ d4422: number;
+ d5220: number;
+ d5232: number;
+ d5421: number;
+ d5433: number;
+ dedt: number;
+ didt: number;
+ dmdt: number;
+ dnodt: number;
+ domdt: number;
+ del1: number;
+ del2: number;
+ del3: number;
+ xfact: number;
+ xlamo: number;
+ xli: number;
+ xni: number;
+}
+export declare function dsinit(options: DsinitOptions): {
+ em: number;
+ argpm: number;
+ inclm: number;
+ mm: number;
+ nm: number;
+ nodem: number;
+ irez: number;
+ atime: number;
+ d2201: number;
+ d2211: number;
+ d3210: number;
+ d3222: number;
+ d4410: number;
+ d4422: number;
+ d5220: number;
+ d5232: number;
+ d5421: number;
+ d5433: number;
+ dedt: number;
+ didt: number;
+ dmdt: number;
+ dndt: number;
+ dnodt: number;
+ domdt: number;
+ del1: number;
+ del2: number;
+ del3: number;
+ xfact: number;
+ xlamo: number;
+ xli: number;
+ xni: number;
+};
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/dsinit.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dsinit.js
new file mode 100644
index 0000000..2f51a6b
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dsinit.js
@@ -0,0 +1,331 @@
+import { pi, twoPi, x2o3, xke } from '../constants.js';
+/*-----------------------------------------------------------------------------
+ *
+ * procedure dsinit
+ *
+ * this procedure provides deep space contributions to mean motion dot due
+ * to geopotential resonance with half day and one day orbits.
+ *
+ * author : david vallado 719-573-2600 28 jun 2005
+ *
+ * inputs :
+ * cosim, sinim-
+ * emsq - eccentricity squared
+ * argpo - argument of perigee
+ * s1, s2, s3, s4, s5 -
+ * ss1, ss2, ss3, ss4, ss5 -
+ * sz1, sz3, sz11, sz13, sz21, sz23, sz31, sz33 -
+ * t - time
+ * tc -
+ * gsto - greenwich sidereal time rad
+ * mo - mean anomaly
+ * mdot - mean anomaly dot (rate)
+ * no - mean motion
+ * nodeo - right ascension of ascending node
+ * nodedot - right ascension of ascending node dot (rate)
+ * xpidot -
+ * z1, z3, z11, z13, z21, z23, z31, z33 -
+ * eccm - eccentricity
+ * argpm - argument of perigee
+ * inclm - inclination
+ * mm - mean anomaly
+ * xn - mean motion
+ * nodem - right ascension of ascending node
+ *
+ * outputs :
+ * em - eccentricity
+ * argpm - argument of perigee
+ * inclm - inclination
+ * mm - mean anomaly
+ * nm - mean motion
+ * nodem - right ascension of ascending node
+ * irez - flag for resonance 0-none, 1-one day, 2-half day
+ * atime -
+ * d2201, d2211, d3210, d3222, d4410, d4422, d5220, d5232, d5421, d5433 -
+ * dedt -
+ * didt -
+ * dmdt -
+ * dndt -
+ * dnodt -
+ * domdt -
+ * del1, del2, del3 -
+ * ses , sghl , sghs , sgs , shl , shs , sis , sls
+ * theta -
+ * xfact -
+ * xlamo -
+ * xli -
+ * xni
+ *
+ * locals :
+ * ainv2 -
+ * aonv -
+ * cosisq -
+ * eoc -
+ * f220, f221, f311, f321, f322, f330, f441, f442, f522, f523, f542, f543 -
+ * g200, g201, g211, g300, g310, g322, g410, g422, g520, g521, g532, g533 -
+ * sini2 -
+ * temp -
+ * temp1 -
+ * theta -
+ * xno2 -
+ *
+ * coupling :
+ * getgravconst
+ *
+ * references :
+ * hoots, roehrich, norad spacetrack report #3 1980
+ * hoots, norad spacetrack report #6 1986
+ * hoots, schumacher and glover 2004
+ * vallado, crawford, hujsak, kelso 2006
+ ----------------------------------------------------------------------------*/
+export function dsinit(options) {
+ const { cosim, argpo, s1, s2, s3, s4, s5, sinim, ss1, ss2, ss3, ss4, ss5, sz1, sz3, sz11, sz13, sz21, sz23, sz31, sz33, t, tc, gsto, mo, mdot, no, nodeo, nodedot, xpidot, z1, z3, z11, z13, z21, z23, z31, z33, ecco, eccsq, } = options;
+ let { emsq, em, argpm, inclm, mm, nm, nodem, irez, atime, d2201, d2211, d3210, d3222, d4410, d4422, d5220, d5232, d5421, d5433, dedt, didt, dmdt, dnodt, domdt, del1, del2, del3, xfact, xlamo, xli, xni, } = options;
+ let f220;
+ let f221;
+ let f311;
+ let f321;
+ let f322;
+ let f330;
+ let f441;
+ let f442;
+ let f522;
+ let f523;
+ let f542;
+ let f543;
+ let g200;
+ let g201;
+ let g211;
+ let g300;
+ let g310;
+ let g322;
+ let g410;
+ let g422;
+ let g520;
+ let g521;
+ let g532;
+ let g533;
+ let sini2;
+ let temp;
+ let temp1;
+ let xno2;
+ let ainv2;
+ let aonv;
+ let cosisq;
+ let eoc;
+ const q22 = 1.7891679e-6;
+ const q31 = 2.1460748e-6;
+ const q33 = 2.2123015e-7;
+ const root22 = 1.7891679e-6;
+ const root44 = 7.3636953e-9;
+ const root54 = 2.1765803e-9;
+ // biome-ignore lint/correctness/noPrecisionLoss: keep original value for search
+ const rptim = 4.37526908801129966e-3; // equates to 7.29211514668855e-5 rad/sec
+ const root32 = 3.7393792e-7;
+ const root52 = 1.1428639e-7;
+ const znl = 1.5835218e-4;
+ const zns = 1.19459e-5;
+ // -------------------- deep space initialization ------------
+ irez = 0;
+ if (nm < 0.0052359877 && nm > 0.0034906585) {
+ irez = 1;
+ }
+ if (nm >= 8.26e-3 && nm <= 9.24e-3 && em >= 0.5) {
+ irez = 2;
+ }
+ // ------------------------ do solar terms -------------------
+ const ses = ss1 * zns * ss5;
+ const sis = ss2 * zns * (sz11 + sz13);
+ const sls = -zns * ss3 * (sz1 + sz3 - 14.0 - 6.0 * emsq);
+ const sghs = ss4 * zns * (sz31 + sz33 - 6.0);
+ let shs = -zns * ss2 * (sz21 + sz23);
+ // sgp4fix for 180 deg incl
+ if (inclm < 5.2359877e-2 || inclm > pi - 5.2359877e-2) {
+ shs = 0.0;
+ }
+ if (sinim !== 0.0) {
+ shs /= sinim;
+ }
+ const sgs = sghs - cosim * shs;
+ // ------------------------- do lunar terms ------------------
+ dedt = ses + s1 * znl * s5;
+ didt = sis + s2 * znl * (z11 + z13);
+ dmdt = sls - znl * s3 * (z1 + z3 - 14.0 - 6.0 * emsq);
+ const sghl = s4 * znl * (z31 + z33 - 6.0);
+ let shll = -znl * s2 * (z21 + z23);
+ // sgp4fix for 180 deg incl
+ if (inclm < 5.2359877e-2 || inclm > pi - 5.2359877e-2) {
+ shll = 0.0;
+ }
+ domdt = sgs + sghl;
+ dnodt = shs;
+ if (sinim !== 0.0) {
+ domdt -= (cosim / sinim) * shll;
+ dnodt += shll / sinim;
+ }
+ // ----------- calculate deep space resonance effects --------
+ const dndt = 0.0;
+ const theta = (gsto + tc * rptim) % twoPi;
+ em += dedt * t;
+ inclm += didt * t;
+ argpm += domdt * t;
+ nodem += dnodt * t;
+ mm += dmdt * t;
+ // sgp4fix for negative inclinations
+ // the following if statement should be commented out
+ // if (inclm < 0.0)
+ // {
+ // inclm = -inclm;
+ // argpm = argpm - pi;
+ // nodem = nodem + pi;
+ // }
+ // -------------- initialize the resonance terms -------------
+ if (irez !== 0) {
+ aonv = (nm / xke) ** x2o3;
+ // ---------- geopotential resonance for 12 hour orbits ------
+ if (irez === 2) {
+ cosisq = cosim * cosim;
+ const emo = em;
+ em = ecco;
+ const emsqo = emsq;
+ emsq = eccsq;
+ eoc = em * emsq;
+ g201 = -0.306 - (em - 0.64) * 0.44;
+ if (em <= 0.65) {
+ g211 = 3.616 - 13.247 * em + 16.29 * emsq;
+ g310 = -19.302 + 117.39 * em - 228.419 * emsq + 156.591 * eoc;
+ g322 = -18.9068 + 109.7927 * em - 214.6334 * emsq + 146.5816 * eoc;
+ g410 = -41.122 + 242.694 * em - 471.094 * emsq + 313.953 * eoc;
+ g422 = -146.407 + 841.88 * em - 1629.014 * emsq + 1083.435 * eoc;
+ g520 = -532.114 + 3017.977 * em - 5740.032 * emsq + 3708.276 * eoc;
+ }
+ else {
+ g211 = -72.099 + 331.819 * em - 508.738 * emsq + 266.724 * eoc;
+ g310 = -346.844 + 1582.851 * em - 2415.925 * emsq + 1246.113 * eoc;
+ g322 = -342.585 + 1554.908 * em - 2366.899 * emsq + 1215.972 * eoc;
+ g410 = -1052.797 + 4758.686 * em - 7193.992 * emsq + 3651.957 * eoc;
+ g422 = -3581.69 + 16178.11 * em - 24462.77 * emsq + 12422.52 * eoc;
+ if (em > 0.715) {
+ g520 = -5149.66 + 29936.92 * em - 54087.36 * emsq + 31324.56 * eoc;
+ }
+ else {
+ g520 = 1464.74 - 4664.75 * em + 3763.64 * emsq;
+ }
+ }
+ if (em < 0.7) {
+ g533 = -919.2277 + 4988.61 * em - 9064.77 * emsq + 5542.21 * eoc;
+ g521 = -822.71072 + 4568.6173 * em - 8491.4146 * emsq + 5337.524 * eoc;
+ g532 = -853.666 + 4690.25 * em - 8624.77 * emsq + 5341.4 * eoc;
+ }
+ else {
+ g533 = -37995.78 + 161616.52 * em - 229838.2 * emsq + 109377.94 * eoc;
+ g521 = -51752.104 + 218913.95 * em - 309468.16 * emsq + 146349.42 * eoc;
+ g532 = -40023.88 + 170470.89 * em - 242699.48 * emsq + 115605.82 * eoc;
+ }
+ sini2 = sinim * sinim;
+ f220 = 0.75 * (1.0 + 2.0 * cosim + cosisq);
+ f221 = 1.5 * sini2;
+ f321 = 1.875 * sinim * (1.0 - 2.0 * cosim - 3.0 * cosisq);
+ f322 = -1.875 * sinim * (1.0 + 2.0 * cosim - 3.0 * cosisq);
+ f441 = 35.0 * sini2 * f220;
+ f442 = 39.375 * sini2 * sini2;
+ f522 =
+ 9.84375 *
+ sinim *
+ (sini2 * (1.0 - 2.0 * cosim - 5.0 * cosisq) +
+ 0.33333333 * (-2.0 + 4.0 * cosim + 6.0 * cosisq));
+ f523 =
+ sinim *
+ (4.92187512 * sini2 * (-2.0 - 4.0 * cosim + 10.0 * cosisq) +
+ 6.56250012 * (1.0 + 2.0 * cosim - 3.0 * cosisq));
+ f542 =
+ 29.53125 *
+ sinim *
+ (2.0 - 8.0 * cosim + cosisq * (-12.0 + 8.0 * cosim + 10.0 * cosisq));
+ f543 =
+ 29.53125 *
+ sinim *
+ (-2.0 - 8.0 * cosim + cosisq * (12.0 + 8.0 * cosim - 10.0 * cosisq));
+ xno2 = nm * nm;
+ ainv2 = aonv * aonv;
+ temp1 = 3.0 * xno2 * ainv2;
+ temp = temp1 * root22;
+ d2201 = temp * f220 * g201;
+ d2211 = temp * f221 * g211;
+ temp1 *= aonv;
+ temp = temp1 * root32;
+ d3210 = temp * f321 * g310;
+ d3222 = temp * f322 * g322;
+ temp1 *= aonv;
+ temp = 2.0 * temp1 * root44;
+ d4410 = temp * f441 * g410;
+ d4422 = temp * f442 * g422;
+ temp1 *= aonv;
+ temp = temp1 * root52;
+ d5220 = temp * f522 * g520;
+ d5232 = temp * f523 * g532;
+ temp = 2.0 * temp1 * root54;
+ d5421 = temp * f542 * g521;
+ d5433 = temp * f543 * g533;
+ xlamo = (mo + nodeo + nodeo - (theta + theta)) % twoPi;
+ xfact = mdot + dmdt + 2.0 * (nodedot + dnodt - rptim) - no;
+ em = emo;
+ emsq = emsqo;
+ }
+ // ---------------- synchronous resonance terms --------------
+ if (irez === 1) {
+ g200 = 1.0 + emsq * (-2.5 + 0.8125 * emsq);
+ g310 = 1.0 + 2.0 * emsq;
+ g300 = 1.0 + emsq * (-6.0 + 6.60937 * emsq);
+ f220 = 0.75 * (1.0 + cosim) * (1.0 + cosim);
+ f311 =
+ 0.9375 * sinim * sinim * (1.0 + 3.0 * cosim) - 0.75 * (1.0 + cosim);
+ f330 = 1.0 + cosim;
+ f330 = 1.875 * f330 * f330 * f330;
+ del1 = 3.0 * nm * nm * aonv * aonv;
+ del2 = 2.0 * del1 * f220 * g200 * q22;
+ del3 = 3.0 * del1 * f330 * g300 * q33 * aonv;
+ del1 = del1 * f311 * g310 * q31 * aonv;
+ xlamo = (mo + nodeo + argpo - theta) % twoPi;
+ xfact = mdot + xpidot + dmdt + domdt + dnodt - (no + rptim);
+ }
+ // ------------ for sgp4, initialize the integrator ----------
+ xli = xlamo;
+ xni = no;
+ atime = 0.0;
+ nm = no + dndt;
+ }
+ return {
+ em,
+ argpm,
+ inclm,
+ mm,
+ nm,
+ nodem,
+ irez,
+ atime,
+ d2201,
+ d2211,
+ d3210,
+ d3222,
+ d4410,
+ d4422,
+ d5220,
+ d5232,
+ d5421,
+ d5433,
+ dedt,
+ didt,
+ dmdt,
+ dndt,
+ dnodt,
+ domdt,
+ del1,
+ del2,
+ del3,
+ xfact,
+ xlamo,
+ xli,
+ xni,
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/dspace.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dspace.d.ts
new file mode 100644
index 0000000..beb0447
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dspace.d.ts
@@ -0,0 +1,51 @@
+interface DspaceOptions {
+ irez: number;
+ d2201: number;
+ d2211: number;
+ d3210: number;
+ d3222: number;
+ d4410: number;
+ d4422: number;
+ d5220: number;
+ d5232: number;
+ d5421: number;
+ d5433: number;
+ dedt: number;
+ del1: number;
+ del2: number;
+ del3: number;
+ didt: number;
+ dmdt: number;
+ dnodt: number;
+ domdt: number;
+ argpo: number;
+ argpdot: number;
+ t: number;
+ tc: number;
+ gsto: number;
+ xfact: number;
+ xlamo: number;
+ no: number;
+ atime: number;
+ em: number;
+ argpm: number;
+ inclm: number;
+ xli: number;
+ mm: number;
+ xni: number;
+ nodem: number;
+ nm: number;
+}
+export declare function dspace(options: DspaceOptions): {
+ atime: number;
+ em: number;
+ argpm: number;
+ inclm: number;
+ xli: number;
+ mm: number;
+ xni: number;
+ nodem: number;
+ dndt: number;
+ nm: number;
+};
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/dspace.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dspace.js
new file mode 100644
index 0000000..1c21044
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/dspace.js
@@ -0,0 +1,226 @@
+/** biome-ignore-all lint/style/noNonNullAssertion: due to the fact that
+ * many variables are first assigned within loops and if statements,
+ * Typescript marks many of them as "used before assigned". To keep the algorithm
+ * as is, non null assertion is used there.
+ */
+import { twoPi } from '../constants.js';
+/*-----------------------------------------------------------------------------
+ *
+ * procedure dspace
+ *
+ * this procedure provides deep space contributions to mean elements for
+ * perturbing third body. these effects have been averaged over one
+ * revolution of the sun and moon. for earth resonance effects, the
+ * effects have been averaged over no revolutions of the satellite.
+ * (mean motion)
+ *
+ * author : david vallado 719-573-2600 28 jun 2005
+ *
+ * inputs :
+ * d2201, d2211, d3210, d3222, d4410, d4422, d5220, d5232, d5421, d5433 -
+ * dedt -
+ * del1, del2, del3 -
+ * didt -
+ * dmdt -
+ * dnodt -
+ * domdt -
+ * irez - flag for resonance 0-none, 1-one day, 2-half day
+ * argpo - argument of perigee
+ * argpdot - argument of perigee dot (rate)
+ * t - time
+ * tc -
+ * gsto - gst
+ * xfact -
+ * xlamo -
+ * no - mean motion
+ * atime -
+ * em - eccentricity
+ * ft -
+ * argpm - argument of perigee
+ * inclm - inclination
+ * xli -
+ * mm - mean anomaly
+ * xni - mean motion
+ * nodem - right ascension of ascending node
+ *
+ * outputs :
+ * atime -
+ * em - eccentricity
+ * argpm - argument of perigee
+ * inclm - inclination
+ * xli -
+ * mm - mean anomaly
+ * xni -
+ * nodem - right ascension of ascending node
+ * dndt -
+ * nm - mean motion
+ *
+ * locals :
+ * delt -
+ * ft -
+ * theta -
+ * x2li -
+ * x2omi -
+ * xl -
+ * xldot -
+ * xnddt -
+ * xndt -
+ * xomi -
+ *
+ * coupling :
+ * none -
+ *
+ * references :
+ * hoots, roehrich, norad spacetrack report #3 1980
+ * hoots, norad spacetrack report #6 1986
+ * hoots, schumacher and glover 2004
+ * vallado, crawford, hujsak, kelso 2006
+ ----------------------------------------------------------------------------*/
+export function dspace(options) {
+ const { irez, d2201, d2211, d3210, d3222, d4410, d4422, d5220, d5232, d5421, d5433, dedt, del1, del2, del3, didt, dmdt, dnodt, domdt, argpo, argpdot, t, tc, gsto, xfact, xlamo, no, } = options;
+ let { atime, em, argpm, inclm, xli, mm, xni, nodem, nm } = options;
+ const fasx2 = 0.13130908;
+ const fasx4 = 2.8843198;
+ const fasx6 = 0.37448087;
+ const g22 = 5.7686396;
+ const g32 = 0.95240898;
+ const g44 = 1.8014998;
+ const g52 = 1.050833;
+ const g54 = 4.4108898;
+ // biome-ignore lint/correctness/noPrecisionLoss: keep original value for search
+ const rptim = 4.37526908801129966e-3; // equates to 7.29211514668855e-5 rad/sec
+ const stepp = 720.0;
+ const stepn = -720.0;
+ const step2 = 259200.0;
+ let delt;
+ let x2li;
+ let x2omi;
+ let xl;
+ let xldot;
+ let xnddt;
+ let xndt;
+ let xomi;
+ let dndt = 0.0;
+ let ft = 0.0;
+ // ----------- calculate deep space resonance effects -----------
+ const theta = (gsto + tc * rptim) % twoPi;
+ em += dedt * t;
+ inclm += didt * t;
+ argpm += domdt * t;
+ nodem += dnodt * t;
+ mm += dmdt * t;
+ // sgp4fix for negative inclinations
+ // the following if statement should be commented out
+ // if (inclm < 0.0)
+ // {
+ // inclm = -inclm;
+ // argpm = argpm - pi;
+ // nodem = nodem + pi;
+ // }
+ /* - update resonances : numerical (euler-maclaurin) integration - */
+ /* ------------------------- epoch restart ---------------------- */
+ // sgp4fix for propagator problems
+ // the following integration works for negative time steps and periods
+ // the specific changes are unknown because the original code was so convoluted
+ // sgp4fix take out atime = 0.0 and fix for faster operation
+ if (irez !== 0) {
+ // sgp4fix streamline check
+ if (atime === 0.0 || t * atime <= 0.0 || Math.abs(t) < Math.abs(atime)) {
+ atime = 0.0;
+ xni = no;
+ xli = xlamo;
+ }
+ // sgp4fix move check outside loop
+ if (t > 0.0) {
+ delt = stepp;
+ }
+ else {
+ delt = stepn;
+ }
+ let iretn = 381; // added for do loop
+ while (iretn === 381) {
+ // ------------------- dot terms calculated -------------
+ // ----------- near - synchronous resonance terms -------
+ if (irez !== 2) {
+ xndt =
+ del1 * Math.sin(xli - fasx2) +
+ del2 * Math.sin(2.0 * (xli - fasx4)) +
+ del3 * Math.sin(3.0 * (xli - fasx6));
+ xldot = xni + xfact;
+ xnddt =
+ del1 * Math.cos(xli - fasx2) +
+ 2.0 * del2 * Math.cos(2.0 * (xli - fasx4)) +
+ 3.0 * del3 * Math.cos(3.0 * (xli - fasx6));
+ xnddt *= xldot;
+ }
+ else {
+ // --------- near - half-day resonance terms --------
+ xomi = argpo + argpdot * atime;
+ x2omi = xomi + xomi;
+ x2li = xli + xli;
+ xndt =
+ d2201 * Math.sin(x2omi + xli - g22) +
+ d2211 * Math.sin(xli - g22) +
+ d3210 * Math.sin(xomi + xli - g32) +
+ d3222 * Math.sin(-xomi + xli - g32) +
+ d4410 * Math.sin(x2omi + x2li - g44) +
+ d4422 * Math.sin(x2li - g44) +
+ d5220 * Math.sin(xomi + xli - g52) +
+ d5232 * Math.sin(-xomi + xli - g52) +
+ d5421 * Math.sin(xomi + x2li - g54) +
+ d5433 * Math.sin(-xomi + x2li - g54);
+ xldot = xni + xfact;
+ xnddt =
+ d2201 * Math.cos(x2omi + xli - g22) +
+ d2211 * Math.cos(xli - g22) +
+ d3210 * Math.cos(xomi + xli - g32) +
+ d3222 * Math.cos(-xomi + xli - g32) +
+ d5220 * Math.cos(xomi + xli - g52) +
+ d5232 * Math.cos(-xomi + xli - g52) +
+ 2.0 *
+ (d4410 * Math.cos(x2omi + x2li - g44) +
+ d4422 * Math.cos(x2li - g44) +
+ d5421 * Math.cos(xomi + x2li - g54) +
+ d5433 * Math.cos(-xomi + x2li - g54));
+ xnddt *= xldot;
+ }
+ // ----------------------- integrator -------------------
+ // sgp4fix move end checks to end of routine
+ if (Math.abs(t - atime) >= stepp) {
+ iretn = 381;
+ }
+ else {
+ ft = t - atime;
+ iretn = 0;
+ }
+ if (iretn === 381) {
+ xli += xldot * delt + xndt * step2;
+ xni += xndt * delt + xnddt * step2;
+ atime += delt;
+ }
+ }
+ nm = xni + xndt * ft + xnddt * ft * ft * 0.5;
+ xl = xli + xldot * ft + xndt * ft * ft * 0.5;
+ if (irez !== 1) {
+ mm = xl - 2.0 * nodem + 2.0 * theta;
+ dndt = nm - no;
+ }
+ else {
+ mm = xl - nodem - argpm + theta;
+ dndt = nm - no;
+ }
+ nm = no + dndt;
+ }
+ return {
+ atime,
+ em,
+ argpm,
+ inclm,
+ xli,
+ mm,
+ xni,
+ nodem,
+ dndt,
+ nm,
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/gstime.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/gstime.d.ts
new file mode 100644
index 0000000..87224dd
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/gstime.d.ts
@@ -0,0 +1,4 @@
+import type { GMSTime } from '../common-types.js';
+export declare function gstime(jd: number): GMSTime;
+export declare function gstime(date: Date): GMSTime;
+export declare function gstime(year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond?: number): GMSTime;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/gstime.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/gstime.js
new file mode 100644
index 0000000..5e1ff25
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/gstime.js
@@ -0,0 +1,51 @@
+import { deg2rad, twoPi } from '../constants.js';
+import { jday } from '../ext.js';
+/* -----------------------------------------------------------------------------
+ *
+ * function gstime
+ *
+ * this function finds the greenwich sidereal time.
+ *
+ * author : david vallado 719-573-2600 1 mar 2001
+ *
+ * inputs description range / units
+ * jdut1 - julian date in ut1 days from 4713 bc
+ *
+ * outputs :
+ * gstime - greenwich sidereal time 0 to 2pi rad
+ *
+ * locals :
+ * temp - temporary variable for doubles rad
+ * tut1 - julian centuries from the
+ * jan 1, 2000 12 h epoch (ut1)
+ *
+ * coupling :
+ * none
+ *
+ * references :
+ * vallado 2004, 191, eq 3-45
+ * --------------------------------------------------------------------------- */
+function gstimeInternal(jdut1) {
+ const tut1 = (jdut1 - 2451545.0) / 36525.0;
+ let temp = -6.2e-6 * tut1 * tut1 * tut1 +
+ 0.093104 * tut1 * tut1 +
+ (876600.0 * 3600 + 8640184.812866) * tut1 +
+ 67310.54841; // # sec
+ temp = ((temp * deg2rad) / 240.0) % twoPi; // 360/86400 = 1/240, to deg, to rad
+ // ------------------------ check quadrants ---------------------
+ if (temp < 0.0) {
+ temp += twoPi;
+ }
+ return temp;
+}
+export function gstime(first, month, day, hour, minute, second, millisecond) {
+ if (first instanceof Date) {
+ return gstimeInternal(jday(first));
+ }
+ if (month !== undefined) {
+ return gstimeInternal(
+ // biome-ignore lint/style/noNonNullAssertion: overloads make sure those are non-null
+ jday(first, month, day, hour, minute, second, millisecond));
+ }
+ return gstimeInternal(first);
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/initl.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/initl.d.ts
new file mode 100644
index 0000000..39d038d
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/initl.d.ts
@@ -0,0 +1,25 @@
+interface InitlOptions {
+ opsmode: 'a' | 'i';
+ ecco: number;
+ epoch: number;
+ inclo: number;
+ no: number;
+}
+export declare function initl(options: InitlOptions): {
+ no: number;
+ method: string;
+ ainv: number;
+ ao: number;
+ con41: number;
+ con42: number;
+ cosio: number;
+ cosio2: number;
+ eccsq: number;
+ omeosq: number;
+ posq: number;
+ rp: number;
+ rteosq: number;
+ sinio: number;
+ gsto: number;
+};
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/initl.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/initl.js
new file mode 100644
index 0000000..df48cd7
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/initl.js
@@ -0,0 +1,124 @@
+import { j2, twoPi, x2o3, xke } from '../constants.js';
+import { gstime } from './gstime.js';
+/*-----------------------------------------------------------------------------
+ *
+ * procedure initl
+ *
+ * this procedure initializes the sgp4 propagator. all the initialization is
+ * consolidated here instead of having multiple loops inside other routines.
+ *
+ * author : david vallado 719-573-2600 28 jun 2005
+ *
+ * inputs :
+ * ecco - eccentricity 0.0 - 1.0
+ * epoch - epoch time in days from jan 0, 1950. 0 hr
+ * inclo - inclination of satellite
+ * no - mean motion of satellite
+ * satn - satellite number
+ *
+ * outputs :
+ * ainv - 1.0 / a
+ * ao - semi major axis
+ * con41 -
+ * con42 - 1.0 - 5.0 cos(i)
+ * cosio - cosine of inclination
+ * cosio2 - cosio squared
+ * eccsq - eccentricity squared
+ * method - flag for deep space 'd', 'n'
+ * omeosq - 1.0 - ecco * ecco
+ * posq - semi-parameter squared
+ * rp - radius of perigee
+ * rteosq - square root of (1.0 - ecco*ecco)
+ * sinio - sine of inclination
+ * gsto - gst at time of observation rad
+ * no - mean motion of satellite
+ *
+ * locals :
+ * ak -
+ * d1 -
+ * del -
+ * adel -
+ * po -
+ *
+ * coupling :
+ * getgravconst
+ * gstime - find greenwich sidereal time from the julian date
+ *
+ * references :
+ * hoots, roehrich, norad spacetrack report #3 1980
+ * hoots, norad spacetrack report #6 1986
+ * hoots, schumacher and glover 2004
+ * vallado, crawford, hujsak, kelso 2006
+ ----------------------------------------------------------------------------*/
+export function initl(options) {
+ const { ecco, epoch, inclo, opsmode } = options;
+ let { no } = options;
+ // sgp4fix use old way of finding gst
+ // ----------------------- earth constants ---------------------
+ // sgp4fix identify constants and allow alternate values
+ // ------------- calculate auxillary epoch quantities ----------
+ const eccsq = ecco * ecco;
+ const omeosq = 1.0 - eccsq;
+ const rteosq = Math.sqrt(omeosq);
+ const cosio = Math.cos(inclo);
+ const cosio2 = cosio * cosio;
+ // ------------------ un-kozai the mean motion -----------------
+ const ak = (xke / no) ** x2o3;
+ const d1 = (0.75 * j2 * (3.0 * cosio2 - 1.0)) / (rteosq * omeosq);
+ let delPrime = d1 / (ak * ak);
+ const adel = ak *
+ (1.0 -
+ delPrime * delPrime -
+ delPrime * (1.0 / 3.0 + (134.0 * delPrime * delPrime) / 81.0));
+ delPrime = d1 / (adel * adel);
+ no /= 1.0 + delPrime;
+ const ao = (xke / no) ** x2o3;
+ const sinio = Math.sin(inclo);
+ const po = ao * omeosq;
+ const con42 = 1.0 - 5.0 * cosio2;
+ const con41 = -con42 - cosio2 - cosio2;
+ const ainv = 1.0 / ao;
+ const posq = po * po;
+ const rp = ao * (1.0 - ecco);
+ const method = 'n';
+ // sgp4fix modern approach to finding sidereal time
+ let gsto;
+ if (opsmode === 'a') {
+ // sgp4fix use old way of finding gst
+ // count integer number of days from 0 jan 1970
+ const ts70 = epoch - 7305.0;
+ const ds70 = Math.floor(ts70 + 1.0e-8);
+ const tfrac = ts70 - ds70;
+ // find greenwich location at epoch
+ // biome-ignore-start lint/correctness/noPrecisionLoss: keep original values for search
+ const c1 = 1.72027916940703639e-2;
+ const thgr70 = 1.7321343856509374;
+ const fk5r = 5.07551419432269442e-15;
+ // biome-ignore-end lint/correctness/noPrecisionLoss: keep original values for search
+ const c1p2p = c1 + twoPi;
+ gsto = (thgr70 + c1 * ds70 + c1p2p * tfrac + ts70 * ts70 * fk5r) % twoPi;
+ if (gsto < 0.0) {
+ gsto += twoPi;
+ }
+ }
+ else {
+ gsto = gstime(epoch + 2433281.5);
+ }
+ return {
+ no,
+ method,
+ ainv,
+ ao,
+ con41,
+ con42,
+ cosio,
+ cosio2,
+ eccsq,
+ omeosq,
+ posq,
+ rp,
+ rteosq,
+ sinio,
+ gsto,
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/propagate.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/propagate.d.ts
new file mode 100644
index 0000000..f37b829
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/propagate.d.ts
@@ -0,0 +1,17 @@
+import type { PositionAndVelocity } from '../common-types.js';
+import { type SatRec } from './SatRec.js';
+export interface PropagateOptions {
+ /**
+ * Opt in to the community fix for satellites that decayed long ago, which SGP4
+ * would otherwise propagate to meaningless positions instead of reporting as
+ * decayed.
+ *
+ * This check is NOT part of the official SGP4 algotithm, so if you need completely compliant
+ * SGP4 output *despite* it sometimes giving garbage positions for decayed satellites,
+ * you should NOT use it.
+ */
+ communityDecayCheckEnabled: boolean;
+}
+export declare function propagate(satrec: SatRec, date: Date, options?: PropagateOptions): PositionAndVelocity | null;
+export declare function propagate(satrec: SatRec, year: number, month: number, day: number, hour: number, minute: number, second: number, options?: PropagateOptions): PositionAndVelocity | null;
+export declare function propagate(satrec: SatRec, year: number, month: number, day: number, hour: number, minute: number, second: number, ms: number | undefined, options?: PropagateOptions): PositionAndVelocity | null;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/propagate.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/propagate.js
new file mode 100644
index 0000000..1f3a9b7
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/propagate.js
@@ -0,0 +1,25 @@
+import { minutesPerDay } from '../constants.js';
+import { jday } from '../ext.js';
+import { checkForDecay } from './check-for-decay.js';
+import { SatRecError } from './SatRec.js';
+import { sgp4 } from './sgp4.js';
+export function propagate(satrec,
+// Deliberately permissive: the overloads above define the public contract, and
+// TypeScript only requires the implementation signature to be compatible with
+// all of them.
+...args) {
+ // Return a position and velocity vector for a given date and time.
+ const last = args.at(-1);
+ const options = typeof last === 'object' && !(last instanceof Date) ? last : undefined;
+ const jdayArgs = (options ? args.slice(0, -1) : args);
+ const j = jday(...jdayArgs);
+ const m = (j - satrec.jdsatepoch) * minutesPerDay;
+ const result = sgp4(satrec, m);
+ // sgp4 sometimes propagates satellites that decayed long ago to meaningless positions
+ // rather than reporting them as decayed; opting in reports them like sgp4 does.
+ if (options?.communityDecayCheckEnabled && result && checkForDecay(satrec)) {
+ satrec.error = SatRecError.Decayed;
+ return null;
+ }
+ return result;
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4.d.ts
new file mode 100644
index 0000000..442a320
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4.d.ts
@@ -0,0 +1,8 @@
+/** biome-ignore-all lint/style/noNonNullAssertion: due to the fact that
+ * many variables are first assigned within loops and if statements,
+ * Typescript marks many of them as "used before assigned". To keep the algorithm
+ * as is, non null assertion is used there.
+ */
+import type { PositionAndVelocity } from '../common-types.js';
+import { type SatRec } from './SatRec.js';
+export declare function sgp4(satrec: SatRec, tsince: number): PositionAndVelocity | null;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4.js
new file mode 100644
index 0000000..003431a
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4.js
@@ -0,0 +1,390 @@
+import { earthRadius, j2, j3oj2, pi, twoPi, vkmpersec, x2o3, xke, } from '../constants.js';
+import { dpper } from './dpper.js';
+import { dspace } from './dspace.js';
+import { SatRecError } from './SatRec.js';
+/*----------------------------------------------------------------------------
+ *
+ * procedure sgp4
+ *
+ * this procedure is the sgp4 prediction model from space command. this is an
+ * updated and combined version of sgp4 and sdp4, which were originally
+ * published separately in spacetrack report //3. this version follows the
+ * methodology from the aiaa paper (2006) describing the history and
+ * development of the code.
+ *
+ * author : david vallado 719-573-2600 28 jun 2005
+ *
+ * inputs :
+ * satrec - initialised structure from sgp4init() call.
+ * tsince - time since epoch (minutes)
+ *
+ * outputs :
+ * r - position vector km
+ * v - velocity km/sec
+ * return code - non-zero on error.
+ * 1 - mean elements, ecc >= 1.0 or ecc < -0.001 or a < 0.95 er
+ * 2 - mean motion less than 0.0
+ * 3 - pert elements, ecc < 0.0 or ecc > 1.0
+ * 4 - semi-latus rectum < 0.0
+ * 5 - epoch elements are sub-orbital
+ * 6 - satellite has decayed
+ *
+ * locals :
+ * am -
+ * axnl, aynl -
+ * betal -
+ * cosim , sinim , cosomm , sinomm , cnod , snod , cos2u ,
+ * sin2u , coseo1 , sineo1 , cosi , sini , cosip , sinip ,
+ * cosisq , cossu , sinsu , cosu , sinu
+ * delm -
+ * delomg -
+ * dndt -
+ * eccm -
+ * emsq -
+ * ecose -
+ * el2 -
+ * eo1 -
+ * eccp -
+ * esine -
+ * argpm -
+ * argpp -
+ * omgadf -
+ * pl -
+ * r -
+ * rtemsq -
+ * rdotl -
+ * rl -
+ * rvdot -
+ * rvdotl -
+ * su -
+ * t2 , t3 , t4 , tc
+ * tem5, temp , temp1 , temp2 , tempa , tempe , templ
+ * u , ux , uy , uz , vx , vy , vz
+ * inclm - inclination
+ * mm - mean anomaly
+ * nm - mean motion
+ * nodem - right asc of ascending node
+ * xinc -
+ * xincp -
+ * xl -
+ * xlm -
+ * mp -
+ * xmdf -
+ * xmx -
+ * xmy -
+ * nodedf -
+ * xnode -
+ * nodep -
+ * np -
+ *
+ * coupling :
+ * getgravconst-
+ * dpper
+ * dspace
+ *
+ * references :
+ * hoots, roehrich, norad spacetrack report //3 1980
+ * hoots, norad spacetrack report //6 1986
+ * hoots, schumacher and glover 2004
+ * vallado, crawford, hujsak, kelso 2006
+ ----------------------------------------------------------------------------*/
+export function sgp4(satrec, tsince) {
+ let coseo1;
+ let sineo1;
+ let cosip;
+ let sinip;
+ let cosisq;
+ let delm;
+ let delomg;
+ let eo1;
+ let argpm;
+ let argpp;
+ let su;
+ let t3;
+ let t4;
+ let tc;
+ let tem5;
+ let temp;
+ let tempa;
+ let tempe;
+ let templ;
+ let inclm;
+ let mm;
+ let nm;
+ let nodem;
+ let xincp;
+ let xlm;
+ let mp;
+ let nodep;
+ /* ------------------ set mathematical constants --------------- */
+ // sgp4fix divisor for divide by zero check on inclination
+ // the old check used 1.0 + cos(pi-1.0e-9), but then compared it to
+ // 1.5 e-12, so the threshold was changed to 1.5e-12 for consistency
+ const temp4 = 1.5e-12;
+ // --------------------- clear sgp4 error flag -----------------
+ satrec.t = tsince;
+ satrec.error = SatRecError.None;
+ // ------- update for secular gravity and atmospheric drag -----
+ const xmdf = satrec.mo + satrec.mdot * satrec.t;
+ const argpdf = satrec.argpo + satrec.argpdot * satrec.t;
+ const nodedf = satrec.nodeo + satrec.nodedot * satrec.t;
+ argpm = argpdf;
+ mm = xmdf;
+ const t2 = satrec.t * satrec.t;
+ nodem = nodedf + satrec.nodecf * t2;
+ tempa = 1.0 - satrec.cc1 * satrec.t;
+ tempe = satrec.bstar * satrec.cc4 * satrec.t;
+ templ = satrec.t2cof * t2;
+ if (satrec.isimp !== 1) {
+ delomg = satrec.omgcof * satrec.t;
+ // sgp4fix use mutliply for speed instead of pow
+ const delmtemp = 1.0 + satrec.eta * Math.cos(xmdf);
+ delm = satrec.xmcof * (delmtemp * delmtemp * delmtemp - satrec.delmo);
+ temp = delomg + delm;
+ mm = xmdf + temp;
+ argpm = argpdf - temp;
+ t3 = t2 * satrec.t;
+ t4 = t3 * satrec.t;
+ tempa = tempa - satrec.d2 * t2 - satrec.d3 * t3 - satrec.d4 * t4;
+ tempe += satrec.bstar * satrec.cc5 * (Math.sin(mm) - satrec.sinmao);
+ templ =
+ templ + satrec.t3cof * t3 + t4 * (satrec.t4cof + satrec.t * satrec.t5cof);
+ }
+ satrec.tempa = tempa;
+ nm = satrec.no;
+ let em = satrec.ecco;
+ inclm = satrec.inclo;
+ if (satrec.method === 'd') {
+ tc = satrec.t;
+ const dspaceOptions = {
+ irez: satrec.irez,
+ d2201: satrec.d2201,
+ d2211: satrec.d2211,
+ d3210: satrec.d3210,
+ d3222: satrec.d3222,
+ d4410: satrec.d4410,
+ d4422: satrec.d4422,
+ d5220: satrec.d5220,
+ d5232: satrec.d5232,
+ d5421: satrec.d5421,
+ d5433: satrec.d5433,
+ dedt: satrec.dedt,
+ del1: satrec.del1,
+ del2: satrec.del2,
+ del3: satrec.del3,
+ didt: satrec.didt,
+ dmdt: satrec.dmdt,
+ dnodt: satrec.dnodt,
+ domdt: satrec.domdt,
+ argpo: satrec.argpo,
+ argpdot: satrec.argpdot,
+ t: satrec.t,
+ tc,
+ gsto: satrec.gsto,
+ xfact: satrec.xfact,
+ xlamo: satrec.xlamo,
+ no: satrec.no,
+ atime: satrec.atime,
+ em,
+ argpm,
+ inclm,
+ xli: satrec.xli,
+ mm,
+ xni: satrec.xni,
+ nodem,
+ nm,
+ };
+ const dspaceResult = dspace(dspaceOptions);
+ ({ em, argpm, inclm, mm, nodem, nm } = dspaceResult);
+ }
+ if (nm <= 0.0) {
+ // printf("// error nm %f\n", nm);
+ satrec.error = SatRecError.MeanMotionBelowZero;
+ // sgp4fix add return
+ return null;
+ }
+ const am = (xke / nm) ** x2o3 * tempa * tempa;
+ nm = xke / am ** 1.5;
+ em -= tempe;
+ // fix tolerance for error recognition
+ // sgp4fix am is fixed from the previous nm check
+ if (em >= 1.0 || em < -0.001) {
+ // || (am < 0.95)
+ // printf("// error em %f\n", em);
+ satrec.error = SatRecError.MeanEccentricityOutOfRange;
+ // sgp4fix to return if there is an error in eccentricity
+ return null;
+ }
+ // sgp4fix fix tolerance to avoid a divide by zero
+ if (em < 1.0e-6) {
+ em = 1.0e-6;
+ }
+ mm += satrec.no * templ;
+ xlm = mm + argpm + nodem;
+ nodem %= twoPi;
+ argpm %= twoPi;
+ xlm %= twoPi;
+ mm = (xlm - argpm - nodem) % twoPi;
+ const meanElements = {
+ am,
+ em,
+ im: inclm,
+ Om: nodem,
+ om: argpm,
+ mm,
+ nm,
+ };
+ // ----------------- compute extra mean quantities -------------
+ const sinim = Math.sin(inclm);
+ const cosim = Math.cos(inclm);
+ // -------------------- add lunar-solar periodics --------------
+ let ep = em;
+ xincp = inclm;
+ argpp = argpm;
+ nodep = nodem;
+ mp = mm;
+ sinip = sinim;
+ cosip = cosim;
+ if (satrec.method === 'd') {
+ const dpperParameters = {
+ inclo: satrec.inclo,
+ init: 'n',
+ ep,
+ inclp: xincp,
+ nodep,
+ argpp,
+ mp,
+ opsmode: satrec.operationmode,
+ };
+ const dpperResult = dpper(satrec, dpperParameters);
+ ({ ep, nodep, argpp, mp } = dpperResult);
+ xincp = dpperResult.inclp;
+ if (xincp < 0.0) {
+ xincp = -xincp;
+ nodep += pi;
+ argpp -= pi;
+ }
+ if (ep < 0.0 || ep > 1.0) {
+ // printf("// error ep %f\n", ep);
+ satrec.error = SatRecError.PerturbedEccentricityOutOfRange;
+ // sgp4fix add return
+ return null;
+ }
+ }
+ // -------------------- long period periodics ------------------
+ if (satrec.method === 'd') {
+ sinip = Math.sin(xincp);
+ cosip = Math.cos(xincp);
+ satrec.aycof = -0.5 * j3oj2 * sinip;
+ // sgp4fix for divide by zero for xincp = 180 deg
+ if (Math.abs(cosip + 1.0) > 1.5e-12) {
+ satrec.xlcof =
+ (-0.25 * j3oj2 * sinip * (3.0 + 5.0 * cosip)) / (1.0 + cosip);
+ }
+ else {
+ satrec.xlcof = (-0.25 * j3oj2 * sinip * (3.0 + 5.0 * cosip)) / temp4;
+ }
+ }
+ const axnl = ep * Math.cos(argpp);
+ temp = 1.0 / (am * (1.0 - ep * ep));
+ const aynl = ep * Math.sin(argpp) + temp * satrec.aycof;
+ const xl = mp + argpp + nodep + temp * satrec.xlcof * axnl;
+ // --------------------- solve kepler's equation ---------------
+ const u = (xl - nodep) % twoPi;
+ eo1 = u;
+ tem5 = 9999.9;
+ let ktr = 1;
+ // sgp4fix for kepler iteration
+ // the following iteration needs better limits on corrections
+ while (Math.abs(tem5) >= 1.0e-12 && ktr <= 10) {
+ sineo1 = Math.sin(eo1);
+ coseo1 = Math.cos(eo1);
+ tem5 = 1.0 - coseo1 * axnl - sineo1 * aynl;
+ tem5 = (u - aynl * coseo1 + axnl * sineo1 - eo1) / tem5;
+ if (Math.abs(tem5) >= 0.95) {
+ if (tem5 > 0.0) {
+ tem5 = 0.95;
+ }
+ else {
+ tem5 = -0.95;
+ }
+ }
+ eo1 += tem5;
+ ktr += 1;
+ }
+ // ------------- short period preliminary quantities -----------
+ const ecose = axnl * coseo1 + aynl * sineo1;
+ const esine = axnl * sineo1 - aynl * coseo1;
+ const el2 = axnl * axnl + aynl * aynl;
+ const pl = am * (1.0 - el2);
+ if (pl < 0.0) {
+ // printf("// error pl %f\n", pl);
+ satrec.error = SatRecError.SemiLatusRectumBelowZero;
+ // sgp4fix add return
+ return null;
+ }
+ const rl = am * (1.0 - ecose);
+ const rdotl = (Math.sqrt(am) * esine) / rl;
+ const rvdotl = Math.sqrt(pl) / rl;
+ const betal = Math.sqrt(1.0 - el2);
+ temp = esine / (1.0 + betal);
+ const sinu = (am / rl) * (sineo1 - aynl - axnl * temp);
+ const cosu = (am / rl) * (coseo1 - axnl + aynl * temp);
+ su = Math.atan2(sinu, cosu);
+ const sin2u = (cosu + cosu) * sinu;
+ const cos2u = 1.0 - 2.0 * sinu * sinu;
+ temp = 1.0 / pl;
+ const temp1 = 0.5 * j2 * temp;
+ const temp2 = temp1 * temp;
+ // -------------- update for short period periodics ------------
+ if (satrec.method === 'd') {
+ cosisq = cosip * cosip;
+ satrec.con41 = 3.0 * cosisq - 1.0;
+ satrec.x1mth2 = 1.0 - cosisq;
+ satrec.x7thm1 = 7.0 * cosisq - 1.0;
+ }
+ const mrt = rl * (1.0 - 1.5 * temp2 * betal * satrec.con41) +
+ 0.5 * temp1 * satrec.x1mth2 * cos2u;
+ // sgp4fix for decaying satellites
+ if (mrt < 1.0) {
+ // printf("// decay condition %11.6f \n",mrt);
+ satrec.error = SatRecError.Decayed;
+ return null;
+ }
+ su -= 0.25 * temp2 * satrec.x7thm1 * sin2u;
+ const xnode = nodep + 1.5 * temp2 * cosip * sin2u;
+ const xinc = xincp + 1.5 * temp2 * cosip * sinip * cos2u;
+ const mvt = rdotl - (nm * temp1 * satrec.x1mth2 * sin2u) / xke;
+ const rvdot = rvdotl + (nm * temp1 * (satrec.x1mth2 * cos2u + 1.5 * satrec.con41)) / xke;
+ // --------------------- orientation vectors -------------------
+ const sinsu = Math.sin(su);
+ const cossu = Math.cos(su);
+ const snod = Math.sin(xnode);
+ const cnod = Math.cos(xnode);
+ const sini = Math.sin(xinc);
+ const cosi = Math.cos(xinc);
+ const xmx = -snod * cosi;
+ const xmy = cnod * cosi;
+ const ux = xmx * sinsu + cnod * cossu;
+ const uy = xmy * sinsu + snod * cossu;
+ const uz = sini * sinsu;
+ const vx = xmx * cossu - cnod * sinsu;
+ const vy = xmy * cossu - snod * sinsu;
+ const vz = sini * cossu;
+ // --------- position and velocity (in km and km/sec) ----------
+ const r = {
+ x: mrt * ux * earthRadius,
+ y: mrt * uy * earthRadius,
+ z: mrt * uz * earthRadius,
+ };
+ const v = {
+ x: (mvt * ux + rvdot * vx) * vkmpersec,
+ y: (mvt * uy + rvdot * vy) * vkmpersec,
+ z: (mvt * uz + rvdot * vz) * vkmpersec,
+ };
+ return {
+ position: r,
+ velocity: v,
+ meanElements,
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4init.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4init.d.ts
new file mode 100644
index 0000000..348eb6b
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4init.d.ts
@@ -0,0 +1,14 @@
+import type { SatRec, SatRecInit } from './SatRec.js';
+export interface Sgp4InitOptions {
+ opsmode: 'a' | 'i';
+ satn: string;
+ epoch: number;
+ xbstar: number;
+ xecco: number;
+ xargpo: number;
+ xinclo: number;
+ xmo: number;
+ xno: number;
+ xnodeo: number;
+}
+export declare function sgp4init(satrecInit: SatRecInit, options: Sgp4InitOptions): asserts satrecInit is SatRec;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4init.js b/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4init.js
new file mode 100644
index 0000000..ebcd5ad
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/propagation/sgp4init.js
@@ -0,0 +1,634 @@
+/** biome-ignore-all lint/style/noNonNullAssertion: due to the fact that
+ * many variables are first assigned within loops and if statements,
+ * Typescript marks many of them as "used before assigned". To keep the algorithm
+ * as is, non null assertion is used there.
+ */
+import { earthRadius, j2, j3oj2, j4, pi, tumin, x2o3 } from '../constants.js';
+import { dpper } from './dpper.js';
+import { dscom } from './dscom.js';
+import { dsinit } from './dsinit.js';
+import { initl } from './initl.js';
+import { sgp4 } from './sgp4.js';
+/*-----------------------------------------------------------------------------
+ *
+ * procedure sgp4init
+ *
+ * this procedure initializes variables for sgp4.
+ *
+ * author : david vallado 719-573-2600 28 jun 2005
+ * author : david vallado 719-573-2600 28 jun 2005
+ *
+ * inputs :
+ * opsmode - mode of operation afspc or improved 'a', 'i'
+ * satn - satellite number
+ * bstar - sgp4 type drag coefficient kg/m2er
+ * ecco - eccentricity
+ * epoch - epoch time in days from jan 0, 1950. 0 hr
+ * argpo - argument of perigee (output if ds)
+ * inclo - inclination
+ * mo - mean anomaly (output if ds)
+ * no - mean motion
+ * nodeo - right ascension of ascending node
+ *
+ * outputs :
+ * rec - common values for subsequent calls
+ * return code - non-zero on error.
+ * 1 - mean elements, ecc >= 1.0 or ecc < -0.001 or a < 0.95 er
+ * 2 - mean motion less than 0.0
+ * 3 - pert elements, ecc < 0.0 or ecc > 1.0
+ * 4 - semi-latus rectum < 0.0
+ * 5 - epoch elements are sub-orbital
+ * 6 - satellite has decayed
+ *
+ * locals :
+ * cnodm , snodm , cosim , sinim , cosomm , sinomm
+ * cc1sq , cc2 , cc3
+ * coef , coef1
+ * cosio4 -
+ * day -
+ * dndt -
+ * em - eccentricity
+ * emsq - eccentricity squared
+ * eeta -
+ * etasq -
+ * gam -
+ * argpm - argument of perigee
+ * nodem -
+ * inclm - inclination
+ * mm - mean anomaly
+ * nm - mean motion
+ * perige - perigee
+ * pinvsq -
+ * psisq -
+ * qzms24 -
+ * rtemsq -
+ * s1, s2, s3, s4, s5, s6, s7 -
+ * sfour -
+ * ss1, ss2, ss3, ss4, ss5, ss6, ss7 -
+ * sz1, sz2, sz3
+ * sz11, sz12, sz13, sz21, sz22, sz23, sz31, sz32, sz33 -
+ * tc -
+ * temp -
+ * temp1, temp2, temp3 -
+ * tsi -
+ * xpidot -
+ * xhdot1 -
+ * z1, z2, z3 -
+ * z11, z12, z13, z21, z22, z23, z31, z32, z33 -
+ *
+ * coupling :
+ * getgravconst-
+ * initl -
+ * dscom -
+ * dpper -
+ * dsinit -
+ * sgp4 -
+ *
+ * references :
+ * hoots, roehrich, norad spacetrack report #3 1980
+ * hoots, norad spacetrack report #6 1986
+ * hoots, schumacher and glover 2004
+ * vallado, crawford, hujsak, kelso 2006
+ ----------------------------------------------------------------------------*/
+export function sgp4init(satrecInit, options) {
+ const { opsmode, satn, epoch, xbstar, xecco, xargpo, xinclo, xmo, xno, xnodeo, } = options;
+ let cosim;
+ let sinim;
+ let cc1sq;
+ let cc2;
+ let cc3;
+ let coef;
+ let coef1;
+ let cosio4;
+ let em;
+ let emsq;
+ let eeta;
+ let etasq;
+ let argpm;
+ let nodem;
+ let inclm;
+ let mm;
+ let nm;
+ let perige;
+ let pinvsq;
+ let psisq;
+ let qzms24;
+ let s1;
+ let s2;
+ let s3;
+ let s4;
+ let s5;
+ let sfour;
+ let ss1;
+ let ss2;
+ let ss3;
+ let ss4;
+ let ss5;
+ let sz1;
+ let sz3;
+ let sz11;
+ let sz13;
+ let sz21;
+ let sz23;
+ let sz31;
+ let sz33;
+ let tc;
+ let temp;
+ let temp1;
+ let temp2;
+ let temp3;
+ let tsi;
+ let xpidot;
+ let xhdot1;
+ let z1;
+ let z3;
+ let z11;
+ let z13;
+ let z21;
+ let z23;
+ let z31;
+ let z33;
+ /* ------------------------ initialization --------------------- */
+ // sgp4fix divisor for divide by zero check on inclination
+ // the old check used 1.0 + Math.cos(pi-1.0e-9), but then compared it to
+ // 1.5 e-12, so the threshold was changed to 1.5e-12 for consistency
+ const temp4 = 1.5e-12;
+ const satrec = satrecInit;
+ // ----------- set all near earth variables to zero ------------
+ satrec.isimp = 0;
+ satrec.method = 'n';
+ satrec.aycof = 0.0;
+ satrec.con41 = 0.0;
+ satrec.cc1 = 0.0;
+ satrec.cc4 = 0.0;
+ satrec.cc5 = 0.0;
+ satrec.d2 = 0.0;
+ satrec.d3 = 0.0;
+ satrec.d4 = 0.0;
+ satrec.delmo = 0.0;
+ satrec.eta = 0.0;
+ satrec.argpdot = 0.0;
+ satrec.omgcof = 0.0;
+ satrec.sinmao = 0.0;
+ satrec.t = 0.0;
+ satrec.t2cof = 0.0;
+ satrec.t3cof = 0.0;
+ satrec.t4cof = 0.0;
+ satrec.t5cof = 0.0;
+ satrec.x1mth2 = 0.0;
+ satrec.x7thm1 = 0.0;
+ satrec.mdot = 0.0;
+ satrec.nodedot = 0.0;
+ satrec.xlcof = 0.0;
+ satrec.xmcof = 0.0;
+ satrec.nodecf = 0.0;
+ // ----------- set all deep space variables to zero ------------
+ satrec.irez = 0;
+ satrec.d2201 = 0.0;
+ satrec.d2211 = 0.0;
+ satrec.d3210 = 0.0;
+ satrec.d3222 = 0.0;
+ satrec.d4410 = 0.0;
+ satrec.d4422 = 0.0;
+ satrec.d5220 = 0.0;
+ satrec.d5232 = 0.0;
+ satrec.d5421 = 0.0;
+ satrec.d5433 = 0.0;
+ satrec.dedt = 0.0;
+ satrec.del1 = 0.0;
+ satrec.del2 = 0.0;
+ satrec.del3 = 0.0;
+ satrec.didt = 0.0;
+ satrec.dmdt = 0.0;
+ satrec.dnodt = 0.0;
+ satrec.domdt = 0.0;
+ satrec.e3 = 0.0;
+ satrec.ee2 = 0.0;
+ satrec.peo = 0.0;
+ satrec.pgho = 0.0;
+ satrec.pho = 0.0;
+ satrec.pinco = 0.0;
+ satrec.plo = 0.0;
+ satrec.se2 = 0.0;
+ satrec.se3 = 0.0;
+ satrec.sgh2 = 0.0;
+ satrec.sgh3 = 0.0;
+ satrec.sgh4 = 0.0;
+ satrec.sh2 = 0.0;
+ satrec.sh3 = 0.0;
+ satrec.si2 = 0.0;
+ satrec.si3 = 0.0;
+ satrec.sl2 = 0.0;
+ satrec.sl3 = 0.0;
+ satrec.sl4 = 0.0;
+ satrec.gsto = 0.0;
+ satrec.xfact = 0.0;
+ satrec.xgh2 = 0.0;
+ satrec.xgh3 = 0.0;
+ satrec.xgh4 = 0.0;
+ satrec.xh2 = 0.0;
+ satrec.xh3 = 0.0;
+ satrec.xi2 = 0.0;
+ satrec.xi3 = 0.0;
+ satrec.xl2 = 0.0;
+ satrec.xl3 = 0.0;
+ satrec.xl4 = 0.0;
+ satrec.xlamo = 0.0;
+ satrec.zmol = 0.0;
+ satrec.zmos = 0.0;
+ satrec.atime = 0.0;
+ satrec.xli = 0.0;
+ satrec.xni = 0.0;
+ // sgp4fix - note the following variables are also passed directly via satrec.
+ // it is possible to streamline the sgp4init call by deleting the "x"
+ // variables, but the user would need to set the satrec.* values first. we
+ // include the additional assignments in case twoline2rv is not used.
+ satrec.bstar = xbstar;
+ satrec.ecco = xecco;
+ satrec.argpo = xargpo;
+ satrec.inclo = xinclo;
+ satrec.mo = xmo;
+ satrec.no = xno;
+ satrec.nokozai = xno;
+ satrec.nodeo = xnodeo;
+ // sgp4fix add opsmode
+ satrec.operationmode = opsmode;
+ // ------------------------ earth constants -----------------------
+ // sgp4fix identify constants and allow alternate values
+ const ss = 78.0 / earthRadius + 1.0;
+ // sgp4fix use multiply for speed instead of pow
+ const qzms2ttemp = (120.0 - 78.0) / earthRadius;
+ const qzms2t = qzms2ttemp * qzms2ttemp * qzms2ttemp * qzms2ttemp;
+ satrec.init = 'y';
+ satrec.t = 0.0;
+ const initlOptions = {
+ satn,
+ ecco: satrec.ecco,
+ epoch,
+ inclo: satrec.inclo,
+ no: satrec.no,
+ method: satrec.method,
+ opsmode: satrec.operationmode,
+ };
+ const initlResult = initl(initlOptions);
+ const { ao, con42, cosio, cosio2, eccsq, omeosq, posq, rp, rteosq, sinio } = initlResult;
+ satrec.no = initlResult.no;
+ satrec.con41 = initlResult.con41;
+ satrec.gsto = initlResult.gsto;
+ satrec.a = (satrec.no * tumin) ** (-2.0 / 3.0);
+ satrec.alta = satrec.a * (1.0 + satrec.ecco) - 1.0;
+ satrec.altp = satrec.a * (1.0 - satrec.ecco) - 1.0;
+ satrec.error = 0;
+ // sgp4fix remove this check as it is unnecessary
+ // the mrt check in sgp4 handles decaying satellite cases even if the starting
+ // condition is below the surface of te earth
+ // if (rp < 1.0)
+ // {
+ // printf("// *** satn%d epoch elts sub-orbital ***\n", satn);
+ // satrec.error = 5;
+ // }
+ if (omeosq >= 0.0 || satrec.no >= 0.0) {
+ satrec.isimp = 0;
+ if (rp < 220.0 / earthRadius + 1.0) {
+ satrec.isimp = 1;
+ }
+ sfour = ss;
+ qzms24 = qzms2t;
+ perige = (rp - 1.0) * earthRadius;
+ // - for perigees below 156 km, s and qoms2t are altered -
+ if (perige < 156.0) {
+ sfour = perige - 78.0;
+ if (perige < 98.0) {
+ sfour = 20.0;
+ }
+ // sgp4fix use multiply for speed instead of pow
+ const qzms24temp = (120.0 - sfour) / earthRadius;
+ qzms24 = qzms24temp * qzms24temp * qzms24temp * qzms24temp;
+ sfour = sfour / earthRadius + 1.0;
+ }
+ pinvsq = 1.0 / posq;
+ tsi = 1.0 / (ao - sfour);
+ satrec.eta = ao * satrec.ecco * tsi;
+ etasq = satrec.eta * satrec.eta;
+ eeta = satrec.ecco * satrec.eta;
+ psisq = Math.abs(1.0 - etasq);
+ coef = qzms24 * tsi ** 4.0;
+ coef1 = coef / psisq ** 3.5;
+ cc2 =
+ coef1 *
+ satrec.no *
+ (ao * (1.0 + 1.5 * etasq + eeta * (4.0 + etasq)) +
+ ((0.375 * j2 * tsi) / psisq) *
+ satrec.con41 *
+ (8.0 + 3.0 * etasq * (8.0 + etasq)));
+ satrec.cc1 = satrec.bstar * cc2;
+ cc3 = 0.0;
+ if (satrec.ecco > 1.0e-4) {
+ cc3 = (-2.0 * coef * tsi * j3oj2 * satrec.no * sinio) / satrec.ecco;
+ }
+ satrec.x1mth2 = 1.0 - cosio2;
+ satrec.cc4 =
+ 2.0 *
+ satrec.no *
+ coef1 *
+ ao *
+ omeosq *
+ (satrec.eta * (2.0 + 0.5 * etasq) +
+ satrec.ecco * (0.5 + 2.0 * etasq) -
+ ((j2 * tsi) / (ao * psisq)) *
+ (-3.0 *
+ satrec.con41 *
+ (1.0 - 2.0 * eeta + etasq * (1.5 - 0.5 * eeta)) +
+ 0.75 *
+ satrec.x1mth2 *
+ (2.0 * etasq - eeta * (1.0 + etasq)) *
+ Math.cos(2.0 * satrec.argpo)));
+ satrec.cc5 =
+ 2.0 * coef1 * ao * omeosq * (1.0 + 2.75 * (etasq + eeta) + eeta * etasq);
+ cosio4 = cosio2 * cosio2;
+ temp1 = 1.5 * j2 * pinvsq * satrec.no;
+ temp2 = 0.5 * temp1 * j2 * pinvsq;
+ temp3 = -0.46875 * j4 * pinvsq * pinvsq * satrec.no;
+ satrec.mdot =
+ satrec.no +
+ 0.5 * temp1 * rteosq * satrec.con41 +
+ 0.0625 * temp2 * rteosq * (13.0 - 78.0 * cosio2 + 137.0 * cosio4);
+ satrec.argpdot =
+ -0.5 * temp1 * con42 +
+ 0.0625 * temp2 * (7.0 - 114.0 * cosio2 + 395.0 * cosio4) +
+ temp3 * (3.0 - 36.0 * cosio2 + 49.0 * cosio4);
+ xhdot1 = -temp1 * cosio;
+ satrec.nodedot =
+ xhdot1 +
+ (0.5 * temp2 * (4.0 - 19.0 * cosio2) +
+ 2.0 * temp3 * (3.0 - 7.0 * cosio2)) *
+ cosio;
+ xpidot = satrec.argpdot + satrec.nodedot;
+ satrec.omgcof = satrec.bstar * cc3 * Math.cos(satrec.argpo);
+ satrec.xmcof = 0.0;
+ if (satrec.ecco > 1.0e-4) {
+ satrec.xmcof = (-x2o3 * coef * satrec.bstar) / eeta;
+ }
+ satrec.nodecf = 3.5 * omeosq * xhdot1 * satrec.cc1;
+ satrec.t2cof = 1.5 * satrec.cc1;
+ // sgp4fix for divide by zero with xinco = 180 deg
+ if (Math.abs(cosio + 1.0) > 1.5e-12) {
+ satrec.xlcof =
+ (-0.25 * j3oj2 * sinio * (3.0 + 5.0 * cosio)) / (1.0 + cosio);
+ }
+ else {
+ satrec.xlcof = (-0.25 * j3oj2 * sinio * (3.0 + 5.0 * cosio)) / temp4;
+ }
+ satrec.aycof = -0.5 * j3oj2 * sinio;
+ // sgp4fix use multiply for speed instead of pow
+ const delmotemp = 1.0 + satrec.eta * Math.cos(satrec.mo);
+ satrec.delmo = delmotemp * delmotemp * delmotemp;
+ satrec.sinmao = Math.sin(satrec.mo);
+ satrec.x7thm1 = 7.0 * cosio2 - 1.0;
+ // --------------- deep space initialization -------------
+ if ((2 * pi) / satrec.no >= 225.0) {
+ satrec.method = 'd';
+ satrec.isimp = 1;
+ tc = 0.0;
+ inclm = satrec.inclo;
+ const dscomOptions = {
+ epoch,
+ ep: satrec.ecco,
+ argpp: satrec.argpo,
+ tc,
+ inclp: satrec.inclo,
+ nodep: satrec.nodeo,
+ np: satrec.no,
+ e3: satrec.e3,
+ ee2: satrec.ee2,
+ peo: satrec.peo,
+ pgho: satrec.pgho,
+ pho: satrec.pho,
+ pinco: satrec.pinco,
+ plo: satrec.plo,
+ se2: satrec.se2,
+ se3: satrec.se3,
+ sgh2: satrec.sgh2,
+ sgh3: satrec.sgh3,
+ sgh4: satrec.sgh4,
+ sh2: satrec.sh2,
+ sh3: satrec.sh3,
+ si2: satrec.si2,
+ si3: satrec.si3,
+ sl2: satrec.sl2,
+ sl3: satrec.sl3,
+ sl4: satrec.sl4,
+ xgh2: satrec.xgh2,
+ xgh3: satrec.xgh3,
+ xgh4: satrec.xgh4,
+ xh2: satrec.xh2,
+ xh3: satrec.xh3,
+ xi2: satrec.xi2,
+ xi3: satrec.xi3,
+ xl2: satrec.xl2,
+ xl3: satrec.xl3,
+ xl4: satrec.xl4,
+ zmol: satrec.zmol,
+ zmos: satrec.zmos,
+ };
+ const dscomResult = dscom(dscomOptions);
+ satrec.e3 = dscomResult.e3;
+ satrec.ee2 = dscomResult.ee2;
+ satrec.peo = dscomResult.peo;
+ satrec.pgho = dscomResult.pgho;
+ satrec.pho = dscomResult.pho;
+ satrec.pinco = dscomResult.pinco;
+ satrec.plo = dscomResult.plo;
+ satrec.se2 = dscomResult.se2;
+ satrec.se3 = dscomResult.se3;
+ satrec.sgh2 = dscomResult.sgh2;
+ satrec.sgh3 = dscomResult.sgh3;
+ satrec.sgh4 = dscomResult.sgh4;
+ satrec.sh2 = dscomResult.sh2;
+ satrec.sh3 = dscomResult.sh3;
+ satrec.si2 = dscomResult.si2;
+ satrec.si3 = dscomResult.si3;
+ satrec.sl2 = dscomResult.sl2;
+ satrec.sl3 = dscomResult.sl3;
+ satrec.sl4 = dscomResult.sl4;
+ ({
+ sinim,
+ cosim,
+ em,
+ emsq,
+ s1,
+ s2,
+ s3,
+ s4,
+ s5,
+ ss1,
+ ss2,
+ ss3,
+ ss4,
+ ss5,
+ sz1,
+ sz3,
+ sz11,
+ sz13,
+ sz21,
+ sz23,
+ sz31,
+ sz33,
+ } = dscomResult);
+ satrec.xgh2 = dscomResult.xgh2;
+ satrec.xgh3 = dscomResult.xgh3;
+ satrec.xgh4 = dscomResult.xgh4;
+ satrec.xh2 = dscomResult.xh2;
+ satrec.xh3 = dscomResult.xh3;
+ satrec.xi2 = dscomResult.xi2;
+ satrec.xi3 = dscomResult.xi3;
+ satrec.xl2 = dscomResult.xl2;
+ satrec.xl3 = dscomResult.xl3;
+ satrec.xl4 = dscomResult.xl4;
+ satrec.zmol = dscomResult.zmol;
+ satrec.zmos = dscomResult.zmos;
+ ({ nm, z1, z3, z11, z13, z21, z23, z31, z33 } = dscomResult);
+ const dpperOptions = {
+ inclo: inclm,
+ init: satrec.init,
+ ep: satrec.ecco,
+ inclp: satrec.inclo,
+ nodep: satrec.nodeo,
+ argpp: satrec.argpo,
+ mp: satrec.mo,
+ opsmode: satrec.operationmode,
+ };
+ const dpperResult = dpper(satrec, dpperOptions);
+ satrec.ecco = dpperResult.ep;
+ satrec.inclo = dpperResult.inclp;
+ satrec.nodeo = dpperResult.nodep;
+ satrec.argpo = dpperResult.argpp;
+ satrec.mo = dpperResult.mp;
+ argpm = 0.0;
+ nodem = 0.0;
+ mm = 0.0;
+ const dsinitOptions = {
+ cosim,
+ emsq,
+ argpo: satrec.argpo,
+ s1: s1,
+ s2: s2,
+ s3: s3,
+ s4: s4,
+ s5: s5,
+ sinim: sinim,
+ ss1: ss1,
+ ss2: ss2,
+ ss3: ss3,
+ ss4: ss4,
+ ss5: ss5,
+ sz1: sz1,
+ sz3: sz3,
+ sz11: sz11,
+ sz13: sz13,
+ sz21: sz21,
+ sz23: sz23,
+ sz31: sz31,
+ sz33: sz33,
+ t: satrec.t,
+ tc,
+ gsto: satrec.gsto,
+ mo: satrec.mo,
+ mdot: satrec.mdot,
+ no: satrec.no,
+ nodeo: satrec.nodeo,
+ nodedot: satrec.nodedot,
+ xpidot: xpidot,
+ z1: z1,
+ z3: z3,
+ z11: z11,
+ z13: z13,
+ z21: z21,
+ z23: z23,
+ z31: z31,
+ z33: z33,
+ ecco: satrec.ecco,
+ eccsq,
+ em,
+ argpm,
+ inclm,
+ mm,
+ nm,
+ nodem,
+ irez: satrec.irez,
+ atime: satrec.atime,
+ d2201: satrec.d2201,
+ d2211: satrec.d2211,
+ d3210: satrec.d3210,
+ d3222: satrec.d3222,
+ d4410: satrec.d4410,
+ d4422: satrec.d4422,
+ d5220: satrec.d5220,
+ d5232: satrec.d5232,
+ d5421: satrec.d5421,
+ d5433: satrec.d5433,
+ dedt: satrec.dedt,
+ didt: satrec.didt,
+ dmdt: satrec.dmdt,
+ dnodt: satrec.dnodt,
+ domdt: satrec.domdt,
+ del1: satrec.del1,
+ del2: satrec.del2,
+ del3: satrec.del3,
+ xfact: satrec.xfact,
+ xlamo: satrec.xlamo,
+ xli: satrec.xli,
+ xni: satrec.xni,
+ };
+ const dsinitResult = dsinit(dsinitOptions);
+ satrec.irez = dsinitResult.irez;
+ satrec.atime = dsinitResult.atime;
+ satrec.d2201 = dsinitResult.d2201;
+ satrec.d2211 = dsinitResult.d2211;
+ satrec.d3210 = dsinitResult.d3210;
+ satrec.d3222 = dsinitResult.d3222;
+ satrec.d4410 = dsinitResult.d4410;
+ satrec.d4422 = dsinitResult.d4422;
+ satrec.d5220 = dsinitResult.d5220;
+ satrec.d5232 = dsinitResult.d5232;
+ satrec.d5421 = dsinitResult.d5421;
+ satrec.d5433 = dsinitResult.d5433;
+ satrec.dedt = dsinitResult.dedt;
+ satrec.didt = dsinitResult.didt;
+ satrec.dmdt = dsinitResult.dmdt;
+ satrec.dnodt = dsinitResult.dnodt;
+ satrec.domdt = dsinitResult.domdt;
+ satrec.del1 = dsinitResult.del1;
+ satrec.del2 = dsinitResult.del2;
+ satrec.del3 = dsinitResult.del3;
+ satrec.xfact = dsinitResult.xfact;
+ satrec.xlamo = dsinitResult.xlamo;
+ satrec.xli = dsinitResult.xli;
+ satrec.xni = dsinitResult.xni;
+ }
+ // ----------- set variables if not deep space -----------
+ if (satrec.isimp !== 1) {
+ cc1sq = satrec.cc1 * satrec.cc1;
+ satrec.d2 = 4.0 * ao * tsi * cc1sq;
+ temp = (satrec.d2 * tsi * satrec.cc1) / 3.0;
+ satrec.d3 = (17.0 * ao + sfour) * temp;
+ satrec.d4 =
+ 0.5 * temp * ao * tsi * (221.0 * ao + 31.0 * sfour) * satrec.cc1;
+ satrec.t3cof = satrec.d2 + 2.0 * cc1sq;
+ satrec.t4cof =
+ 0.25 *
+ (3.0 * satrec.d3 + satrec.cc1 * (12.0 * satrec.d2 + 10.0 * cc1sq));
+ satrec.t5cof =
+ 0.2 *
+ (3.0 * satrec.d4 +
+ 12.0 * satrec.cc1 * satrec.d3 +
+ 6.0 * satrec.d2 * satrec.d2 +
+ 15.0 * cc1sq * (2.0 * satrec.d2 + cc1sq));
+ }
+ /* finally propogate to zero epoch to initialize all others. */
+ // sgp4fix take out check to let satellites process until they are actually below earth surface
+ // if(satrec.error == 0)
+ }
+ sgp4(satrec, 0);
+ satrec.init = 'n';
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/shadow.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/shadow.d.ts
new file mode 100644
index 0000000..8089ce9
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/shadow.d.ts
@@ -0,0 +1,10 @@
+import type { AU, EciVec3, Kilometer } from './common-types.js';
+/**
+ * Calculate the fraction of the Sun's disc obscured by the Earth as seen from a satellite.
+ *
+ * @param sunEciAU - Sun position in AU, as returned by `sunPos().rsun`
+ * @param satelliteEciKm - Satellite position in the ECI frame (km)
+ * @returns Shadow fraction: 0 = fully lit, 1 = umbra,
+ * values between 0 and 1 indicate the fraction of the Sun covered by Earth.
+ */
+export declare function shadowFraction(sunEciAU: EciVec3, satelliteEciKm: EciVec3): number;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/shadow.js b/Petabit/wwwroot/lib/satellite-js/dist/shadow.js
new file mode 100644
index 0000000..97e3a92
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/shadow.js
@@ -0,0 +1,70 @@
+import { earthRadius } from './constants.js';
+const SUN_RADIUS = 695700;
+const KM_PER_AU = 149597870.69098932;
+function vecLength(v) {
+ return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
+}
+function vecDot(a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+}
+function vecNegate(v) {
+ return [-v[0], -v[1], -v[2]];
+}
+function vecNormalize(v) {
+ const len = vecLength(v);
+ return [v[0] / len, v[1] / len, v[2] / len];
+}
+function vecScale(v, s) {
+ return [v[0] * s, v[1] * s, v[2] * s];
+}
+/**
+ * Calculate the fraction of the Sun's disc obscured by the Earth as seen from a satellite.
+ *
+ * @param sunEciAU - Sun position in AU, as returned by `sunPos().rsun`
+ * @param satelliteEciKm - Satellite position in the ECI frame (km)
+ * @returns Shadow fraction: 0 = fully lit, 1 = umbra,
+ * values between 0 and 1 indicate the fraction of the Sun covered by Earth.
+ */
+export function shadowFraction(sunEciAU, satelliteEciKm) {
+ // Sun position in km (ECI)
+ const sunECIinKM = vecScale([sunEciAU.x, sunEciAU.y, sunEciAU.z], KM_PER_AU);
+ // Antisolar direction (unit vector pointing away from the Sun)
+ const antisolar = vecNormalize(vecNegate(sunECIinKM));
+ const positionVec = [
+ satelliteEciKm.x,
+ satelliteEciKm.y,
+ satelliteEciKm.z,
+ ];
+ const positionLength = vecLength(positionVec);
+ const positionAndAntisolarDot = vecDot(positionVec, antisolar);
+ // means the satellite is on the Sun side of the Earth
+ if (positionAndAntisolarDot <= 0) {
+ return 0;
+ }
+ // angular radius of Earth and Sun as seen from the satellite
+ const rE = Math.asin(earthRadius / positionLength);
+ const rS = Math.asin(SUN_RADIUS / vecLength(sunECIinKM));
+ // satellite-earth-antisolar angle,
+ // which is equal to sun-satellite-earth angle (as vertically opposite angles)
+ // i.e. the angle between the centers of earth and sun as seen from the satellite
+ const d = Math.acos(positionAndAntisolarDot / positionLength);
+ // fully inside shadow cone if the angular distance between the centers of Earth and Sun
+ // is less Earth angular radius minus Sun angular radius
+ if (d <= rE - rS) {
+ return 1;
+ }
+ // fully outside shadow cone if the angular distance between the centers of Earth and Sun
+ // is greater than Earth angular radius plus Sun angular radius
+ if (d >= rE + rS) {
+ return 0;
+ }
+ // otherwise in penumbra, calculate the fraction of the Sun's disc obscured by the Earth using
+ // circle–circle intersection area formula
+ const part1 = rS * rS * Math.acos((d * d + rS * rS - rE * rE) / (2 * d * rS));
+ const part2 = rE * rE * Math.acos((d * d + rE * rE - rS * rS) / (2 * d * rE));
+ const part3 = 0.5 *
+ Math.sqrt((-d + rS + rE) * (d + rS - rE) * (d - rS + rE) * (d + rS + rE));
+ const overlapArea = part1 + part2 - part3;
+ const sunDiscArea = Math.PI * rS * rS;
+ return overlapArea / sunDiscArea;
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/sun.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/sun.d.ts
new file mode 100644
index 0000000..ac340db
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/sun.d.ts
@@ -0,0 +1,7 @@
+import type { AU, EciVec3 } from './common-types.js';
+import type { JDay } from './ext.js';
+export declare function sunPos(jday: JDay): {
+ rsun: EciVec3;
+ rtasc: number;
+ decl: number;
+};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/sun.js b/Petabit/wwwroot/lib/satellite-js/dist/sun.js
new file mode 100644
index 0000000..72b2517
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/sun.js
@@ -0,0 +1,129 @@
+import { deg2rad, pi, twoPi } from './constants.js';
+/* Line by Line MATLAB-to-Javascript conversion of "sun.mat" from Vallado package */
+/* -----------------------------------------------------------------------------
+ *
+ * function sunPos
+ *
+ * this function calculates the geocentric equatorial position vector
+ * the sun given the julian date. this is the low precision formula and
+ * is valid for years from 1950 to 2050. accuaracy of apparent coordinates
+ * is 0.01 degrees. notice many of the calculations are performed in
+ * degrees, and are not changed until later. this is due to the fact that
+ * the almanac uses degrees exclusively in their formulations.
+ *
+ * author : david vallado 719-573-2600 1 mar 2001
+ *
+ * inputs description range / units
+ * jd - julian date days from 4713 bc
+ *
+ * outputs :
+ * rsun - ijk position vector of the sun au
+ * rtasc - right ascension rad
+ * decl - declination rad
+ *
+ * coupling :
+ * -
+ *
+ * references :
+ * VALLADO, DAVID A. (2022) ‘Computer software in MATLAB’,
+ * in Fundamentals of astrodynamics and applications. 5th edn.
+ * Computer software in MATLAB: http://celestrak.org/software/vallado-sw.php
+ * --------------------------------------------------------------------------- */
+export function sunPos(jday) {
+ // ------------------------- implementation -----------------
+ // ------------------- initialize values --------------------
+ const tut1 = (jday - 2451545) / 36525;
+ const meanlong = (280.46 + 36000.77 * tut1) % 360; // deg
+ let meananomaly = ((357.5277233 + 35999.05034 * tut1) * deg2rad) % twoPi; // rad
+ if (meananomaly < 0) {
+ meananomaly += twoPi;
+ }
+ const eclplong_raw = ((meanlong +
+ 1.914666471 * Math.sin(meananomaly) +
+ 0.019994643 * Math.sin(2.0 * meananomaly)) %
+ 360.0) *
+ deg2rad; // rad
+ const obliquity = (23.439291 - 0.0130042 * tut1) * deg2rad; // rad
+ // --------- find magnitude of sun vector, and it's components ------
+ const magr = 1.000140612 -
+ 0.016708617 * Math.cos(meananomaly) -
+ 0.000139589 * Math.cos(2.0 * meananomaly); // in au's
+ const rsun = {
+ x: magr * Math.cos(eclplong_raw),
+ y: magr * Math.cos(obliquity) * Math.sin(eclplong_raw),
+ z: magr * Math.sin(obliquity) * Math.sin(eclplong_raw),
+ };
+ const rtasc_raw = Math.atan(Math.cos(obliquity) * Math.tan(eclplong_raw));
+ // --- check that rtasc is in the same quadrant as eclplong_raw ----
+ let eclplong = eclplong_raw;
+ if (eclplong < 0.0) {
+ eclplong += twoPi; // make sure it's in 0 to 2pi range
+ }
+ let rtasc = rtasc_raw;
+ if (Math.abs(eclplong_raw - rtasc) > pi * 0.5) {
+ rtasc += 0.5 * pi * Math.round((eclplong_raw - rtasc_raw) / (0.5 * pi));
+ }
+ const decl = Math.asin(Math.sin(obliquity) * Math.sin(eclplong_raw));
+ return { rsun, rtasc, decl };
+}
+/* Original MATLAB code for Sun position from Vallado package (sun.mat) */
+/*
+ function [rsun,rtasc,decl] = sun ( jd );
+
+ twopi = 2.0*pi;
+ deg2rad = pi/180.0;
+ show = 'n';
+
+ % ------------------------- implementation -----------------
+ % ------------------- initialize values --------------------
+ tut1= ( jd - 2451545.0 )/ 36525.0;
+
+ if show == 'y'
+ fprintf(1,'tut1 %14.9f \n',tut1);
+ end
+
+ meanlong= 280.460 + 36000.77*tut1;
+ meanlong= rem( meanlong,360.0 ); %deg
+
+ ttdb= tut1;
+ meananomaly= 357.5277233 + 35999.05034 *ttdb;
+ meananomaly= rem( meananomaly*deg2rad,twopi ); %rad
+ if ( meananomaly < 0.0 )
+ meananomaly= twopi + meananomaly;
+ end
+
+ eclplong_raw= meanlong + 1.914666471 *sin(meananomaly) ...
+ + 0.019994643 *sin(2.0 *meananomaly); %deg
+ eclplong_raw= rem( eclplong_raw,360.0 ); %deg
+
+ obliquity= 23.439291 - 0.0130042 *ttdb; %deg
+
+ eclplong_raw = eclplong_raw *deg2rad;
+ obliquity= obliquity *deg2rad;
+
+ % --------- find magnitude of sun vector, ) components ------
+ magr= 1.000140612 - 0.016708617 *cos( meananomaly ) ...
+ - 0.000139589 *cos( 2.0 *meananomaly ); % in au's
+
+ rsun(1)= magr*cos( eclplong_raw );
+ rsun(2)= magr*cos(obliquity)*sin(eclplong_raw);
+ rsun(3)= magr*sin(obliquity)*sin(eclplong_raw);
+
+ if show == 'y'
+ fprintf(1,'meanlon %11.6f meanan %11.6f eclplon %11.6f obli %11.6f \n', ...
+ meanlong,meananomaly/deg2rad,eclplong_raw/deg2rad,obliquity/deg2rad);
+ fprintf(1,'rs %11.9f %11.9f %11.9f \n',rsun);
+ fprintf(1,'magr %14.7f \n',magr);
+ end
+
+ rtasc= atan( cos(obliquity)*tan(eclplong_raw) );
+
+ % --- check that rtasc is in the same quadrant as eclplong_raw ----
+ if ( eclplong_raw < 0.0 )
+ eclplong_raw= eclplong_raw + twopi; % make sure it's in 0 to 2pi range
+ end
+ if ( abs( eclplong_raw-rtasc ) > pi*0.5 )
+ rtasc= rtasc + 0.5 *pi*round( (eclplong_raw-rtasc)/(0.5 *pi));
+ end
+ decl = asin( sin(obliquity)*sin(eclplong_raw) );
+ */
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/transforms.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/transforms.d.ts
new file mode 100644
index 0000000..0e9bafb
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/transforms.d.ts
@@ -0,0 +1,12 @@
+import type { Degrees, EcfVec3, EciVec3, GeodeticLocation, GMSTime, Kilometer, LookAngles, Radians } from './common-types.js';
+export declare function radiansToDegrees(radians: Radians): Degrees;
+export declare function degreesToRadians(degrees: Degrees): Radians;
+export declare function degreesLat(radians: Radians): Degrees;
+export declare function degreesLong(radians: Radians): Degrees;
+export declare function radiansLat(degrees: Degrees): Radians;
+export declare function radiansLong(degrees: Degrees): Radians;
+export declare function geodeticToEcf({ longitude, latitude, height, }: GeodeticLocation): EcfVec3;
+export declare function eciToGeodetic(eci: EciVec3, gmst: GMSTime): GeodeticLocation;
+export declare function ecfToEci(ecf: EcfVec3, gmst: GMSTime): EciVec3;
+export declare function eciToEcf(eci: EciVec3, gmst: GMSTime): EcfVec3;
+export declare function ecfToLookAngles(observerGeodetic: GeodeticLocation, satelliteEcf: EcfVec3): LookAngles;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/transforms.js b/Petabit/wwwroot/lib/satellite-js/dist/transforms.js
new file mode 100644
index 0000000..27b24f2
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/transforms.js
@@ -0,0 +1,139 @@
+import { deg2rad, pi, rad2deg, twoPi } from './constants.js';
+export function radiansToDegrees(radians) {
+ return radians * rad2deg;
+}
+export function degreesToRadians(degrees) {
+ return degrees * deg2rad;
+}
+export function degreesLat(radians) {
+ if (radians < -pi / 2 || radians > pi / 2) {
+ throw new RangeError('Latitude radians must be in range [-pi/2; pi/2].');
+ }
+ return radiansToDegrees(radians);
+}
+export function degreesLong(radians) {
+ if (radians < -pi || radians > pi) {
+ throw new RangeError('Longitude radians must be in range [-pi; pi].');
+ }
+ return radiansToDegrees(radians);
+}
+export function radiansLat(degrees) {
+ if (degrees < -90 || degrees > 90) {
+ throw new RangeError('Latitude degrees must be in range [-90; 90].');
+ }
+ return degreesToRadians(degrees);
+}
+export function radiansLong(degrees) {
+ if (degrees < -180 || degrees > 180) {
+ throw new RangeError('Longitude degrees must be in range [-180; 180].');
+ }
+ return degreesToRadians(degrees);
+}
+export function geodeticToEcf({ longitude, latitude, height, }) {
+ const a = 6378.137;
+ const b = 6356.7523142;
+ const f = (a - b) / a;
+ const e2 = 2 * f - f * f;
+ const normal = a / Math.sqrt(1 - e2 * (Math.sin(latitude) * Math.sin(latitude)));
+ const x = (normal + height) * Math.cos(latitude) * Math.cos(longitude);
+ const y = (normal + height) * Math.cos(latitude) * Math.sin(longitude);
+ const z = (normal * (1 - e2) + height) * Math.sin(latitude);
+ return {
+ x,
+ y,
+ z,
+ };
+}
+export function eciToGeodetic(eci, gmst) {
+ // http://www.celestrak.com/columns/v02n03/
+ const a = 6378.137;
+ const b = 6356.7523142;
+ const R = Math.sqrt(eci.x * eci.x + eci.y * eci.y);
+ const f = (a - b) / a;
+ const e2 = 2 * f - f * f;
+ // the one-liner below is an alternative to the loops approach used originally:
+ // let longitude = Math.atan2(eci.y, eci.x) - gmst;
+ // while (longitude < -pi) {
+ // longitude += twoPi;
+ // }
+ // while (longitude > pi) {
+ // longitude -= twoPi;
+ // }
+ const longitude = ((((Math.atan2(eci.y, eci.x) - gmst + pi) % twoPi) + twoPi) % twoPi) - pi;
+ const kmax = 20;
+ let k = 0;
+ let latitude = Math.atan2(eci.z, Math.sqrt(eci.x * eci.x + eci.y * eci.y));
+ let C = 0;
+ while (k++ < kmax) {
+ C = 1 / Math.sqrt(1 - e2 * (Math.sin(latitude) * Math.sin(latitude)));
+ latitude = Math.atan2(eci.z + a * C * e2 * Math.sin(latitude), R);
+ }
+ const height = R / Math.cos(latitude) - a * C;
+ return { longitude, latitude, height };
+}
+export function ecfToEci(ecf, gmst) {
+ // ccar.colorado.edu/ASEN5070/handouts/coordsys.doc
+ //
+ // [X] [C -S 0][X]
+ // [Y] = [S C 0][Y]
+ // [Z]eci [0 0 1][Z]ecf
+ //
+ const X = ecf.x * Math.cos(gmst) - ecf.y * Math.sin(gmst);
+ const Y = ecf.x * Math.sin(gmst) + ecf.y * Math.cos(gmst);
+ const Z = ecf.z;
+ return { x: X, y: Y, z: Z };
+}
+export function eciToEcf(eci, gmst) {
+ // ccar.colorado.edu/ASEN5070/handouts/coordsys.doc
+ //
+ // [X] [C -S 0][X]
+ // [Y] = [S C 0][Y]
+ // [Z]eci [0 0 1][Z]ecf
+ //
+ //
+ // Inverse:
+ // [X] [C S 0][X]
+ // [Y] = [-S C 0][Y]
+ // [Z]ecf [0 0 1][Z]eci
+ const x = eci.x * Math.cos(gmst) + eci.y * Math.sin(gmst);
+ const y = eci.x * -Math.sin(gmst) + eci.y * Math.cos(gmst);
+ const { z } = eci;
+ return {
+ x,
+ y,
+ z,
+ };
+}
+function topocentric(observerGeodetic, satelliteEcf) {
+ // http://www.celestrak.com/columns/v02n02/
+ // TS Kelso's method, except I'm using ECF frame
+ // and he uses ECI.
+ const { longitude, latitude } = observerGeodetic;
+ const observerEcf = geodeticToEcf(observerGeodetic);
+ const rx = satelliteEcf.x - observerEcf.x;
+ const ry = satelliteEcf.y - observerEcf.y;
+ const rz = satelliteEcf.z - observerEcf.z;
+ const topS = Math.sin(latitude) * Math.cos(longitude) * rx +
+ Math.sin(latitude) * Math.sin(longitude) * ry -
+ Math.cos(latitude) * rz;
+ const topE = -Math.sin(longitude) * rx + Math.cos(longitude) * ry;
+ const topZ = Math.cos(latitude) * Math.cos(longitude) * rx +
+ Math.cos(latitude) * Math.sin(longitude) * ry +
+ Math.sin(latitude) * rz;
+ return { topS, topE, topZ };
+}
+function topocentricToLookAngles(tc) {
+ const { topS, topE, topZ } = tc;
+ const rangeSat = Math.sqrt(topS * topS + topE * topE + topZ * topZ);
+ const El = Math.asin(topZ / rangeSat);
+ const Az = Math.atan2(-topE, topS) + pi;
+ return {
+ azimuth: Az,
+ elevation: El,
+ rangeSat, // Range in km
+ };
+}
+export function ecfToLookAngles(observerGeodetic, satelliteEcf) {
+ const topocentricCoords = topocentric(observerGeodetic, satelliteEcf);
+ return topocentricToLookAngles(topocentricCoords);
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/bulk-propagator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/bulk-propagator.d.ts
new file mode 100644
index 0000000..808f69e
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/bulk-propagator.d.ts
@@ -0,0 +1,341 @@
+import type { SatRec } from '../propagation/SatRec.js';
+import type { Calculator } from './calculators/calculator-interface.js';
+import type { TupleOf } from './calculators/tuple-of.js';
+import type { MultiThreadRuntime, WasmRuntime } from './runtimes/wasm-runtime.js';
+import type { TypedArray } from './typed-array.js';
+/**
+ * An object type with no properties, used on the *left* of `extends` to test
+ * whether some other type has any required properties.
+ *
+ * Note this is deliberately not the same as {@link NoRunParameters}: as an
+ * assignment *target* this type accepts excess properties, so it cannot be used
+ * to reject unwanted keys.
+ */
+type EmptyObject = Record;
+/**
+ * Accepts `{}` and nothing else: any property at all fails to be assignable to
+ * `never`. Used as the argument type of `BulkPropagator.run()` when no calculator
+ * requires parameters, so that stray calculator keys are still rejected.
+ */
+type NoRunParameters = Record;
+/**
+ * The widest `Calculator` shape: matches any calculator regardless of its name,
+ * dependencies, outputs or run parameters. Used purely as a generic constraint.
+ */
+type AnyCalculator = Calculator, TypedArray | Record, unknown, Record>;
+export type CalculatorsToFormattedOutput = {
+ [K in Calculators[number]['name']]: ReturnType['getFormattedOutput']>;
+};
+export type CalculatorsToRawOutput = {
+ [K in Calculators[number]['name']]: ReturnType['getRawOutput']>;
+};
+type RunParametersOf = C extends Calculator, TypedArray | Record, unknown, infer RunParameters> ? RunParameters : never;
+type NameOf = C extends {
+ name: infer Name extends string;
+} ? Name : never;
+/**
+ * The explicitly declared keys of `T`, ignoring those contributed by a string
+ * index signature. `Record` - the `Calculator` default, meaning
+ * "takes no run parameters" - yields `never`, whereas `{ enabled?: boolean }`
+ * yields `'enabled'`. This is what separates a calculator that accepts nothing
+ * from one that accepts only optional parameters.
+ */
+type DeclaredKeys = string extends keyof T ? never : keyof T;
+/** True when `T` has at least one *required* property. */
+type HasRequiredRunParams = EmptyObject extends T ? false : true;
+/** True when `T` declares any property at all, required or optional. */
+type HasAnyRunParams = [DeclaredKeys] extends [never] ? false : true;
+/**
+ * Calculators that *must* be given parameters, because at least one property of
+ * their run parameters is required. Their key is required in `run()`.
+ */
+type RequiredRunParameters = {
+ [C in Calculators[number] as HasRequiredRunParams> extends true ? NameOf : never]: RunParametersOf;
+};
+/**
+ * Calculators that *may* be given parameters: they declare properties, but every
+ * one of them is optional. Their key is optional in `run()` - passable, but never
+ * forced. Calculators that declare no properties at all appear in neither map, so
+ * their key stays rejected.
+ */
+type OptionalRunParameters = {
+ [C in Calculators[number] as HasRequiredRunParams> extends true ? never : HasAnyRunParams> extends true ? NameOf : never]?: RunParametersOf;
+};
+/**
+ * The parameter list of `BulkPropagator.run()`, expressed as a rest tuple so the
+ * argument itself can be required or optional. Each configured calculator falls
+ * into exactly one of three buckets, based on its run parameters:
+ *
+ * - at least one required property -> its key is **required**
+ * - only optional properties -> its key is **optional**, but accepted
+ * - no properties at all -> its key is **rejected**
+ *
+ * The argument as a whole is required only if some calculator's key is required.
+ * Passing `{}` is always allowed.
+ */
+type BulkPropagatorRunArgs = RequiredRunParameters extends infer Required ? OptionalRunParameters extends infer Optional ? EmptyObject extends Required ? [DeclaredKeys] extends [never] ? [runParameters?: NoRunParameters] : [runParameters?: Optional] : [runParameters: Required & Optional] : never : never;
+/**
+ * This class allows faster propagation of multiple satellites and multiple time points
+ * by batching calculations and leveraging WASM. It supports configurable set of calculators.
+ *
+ * Kitchen sink example to calculate Look Angles:
+ * @example
+ * ```typescript
+ * import {
+ * createWasmModule,
+ * BulkPropagator,
+ * EciBaseCalculator, EcfPositionCalculator, GmstCalculator, LookAnglesCalculator
+ * } from 'satellite.js';
+ *
+ * const wasmModule = await createWasmModule();
+ * const satellites = [twoline2satrec(line1, line2)];
+ * const dates = [new Date()];
+ * const observerGeodetic = {
+ * latitude: degreesToRadians(41),
+ * longitude: degreesToRadians(-71),
+ * height: 0.2,
+ * };
+ *
+ * // Important: either use `using` syntax OR call `propagator.dispose()` in the end, otherwise
+ * // your app WILL LEAK MEMORY. Propagator disposal is your responsibility!
+ * using propagator = new BulkPropagator({
+ * wasmModule,
+ * calculators: [
+ * new EciBaseCalculator(), // SGP4 output on which further calculators depend
+ * new GmstCalculator(), // dependency of EcfPosition
+ * new EcfPositionCalculator(), // depends on EciBase and Gmst; dependency of LookAngles
+ * // depends on EcfPosition; requires observer parameter in propagator.run()
+ * new LookAnglesCalculator(),
+ * ],
+ * satRecsCount: satellites.length,
+ * datesCount: dates.length,
+ * });
+ *
+ * propagator.setSatRecs(satellites);
+ * propagator.setDates(dates);
+ * propagator.run({ lookAngles: { observer: observerGeodetic } });
+ *
+ * // Get formatted output for satellite 0 at date 0
+ * const result = propagator.getFormattedOutput(0, 0);
+ * console.log(result?.lookAngles.azimuth); // radians
+ * console.log(result?.lookAngles.elevation); // radians
+ *
+ * // Get raw output in form of TypedArrays for further processing.
+ * // Results are sorted by satellite order and then by date order:
+ * // [satellite 0 date 0, satellite 0 date 1, ..., satellite 1 date 0, satellite 1 date 1, ...]
+ * const rawResult = propagator.getRawOutput();
+ * rawResult.lookAngles; // Float64Array
+ * const [azimuth, elevation, rangeSat] = rawResult.lookAngles;
+ *
+ * // The approach above is the same as pure javascript below,
+ * // but BulkPropagator is much more performant.
+ * const results = [];
+ * for (const satellite of satellites) {
+ * for (const date of dates) {
+ * const result = propagate(satellite, date);
+ * const ecf = eciToEcf(result!.position, gstime(date));
+ * const { azimuth, elevation, rangeSat } = ecfToLookAngles(observerGeodetic, ecf);
+ * }
+ * }
+ * ```
+ *
+ * @template Calculators - Readonly array of Calculator instances
+ */
+export declare class BulkPropagator, TypedArray | Record, unknown>[], Runtime extends WasmRuntime> implements Disposable {
+ private readonly calculators;
+ private satrecsPointer;
+ private allocatedSatrecsCount;
+ private usedSatrecsCount;
+ private datesPointer;
+ private allocatedDatesCount;
+ private usedDatesCount;
+ private readonly runtime;
+ private readonly runDataPointer;
+ private outputPointer;
+ private allocatedOutputSizeBytes;
+ private outputPointersByCalculator;
+ private calculatorDependenciesOutputsPointers;
+ private isDisposed;
+ private isRunning;
+ private runCompletionPromise;
+ private needsOutputRedistribution;
+ private hasSatRecs;
+ private hasDates;
+ /**
+ * Creates a BulkPropagator instance.
+ * The BulkPropagator is generic depending on the Calculator instances passed to it.
+ * The return types of `getFormattedOutput()`, `getRawOutput()`, and argument type of `run()`
+ * depend on the passed Calculators.
+ *
+ * @param options - Configuration options
+ * @param options.wasmModule - The WebAssembly module instance
+ * (use `createWasmModule()` to create one and reuse it)
+ * @param options.calculators - Array of calculator instances
+ * to run during propagation; they all named as `*Calculator` for easy discovery
+ * @param options.satRecsCount - Initial allocation size for satellite records
+ * @param options.datesCount - Initial allocation size for dates
+ *
+ * @example
+ * ```ts
+ * const propagator = new BulkPropagator({
+ * wasmModule: await createWasmModule(),
+ * calculators: [new EciPositionCalculator()],
+ * satRecsCount: 60, // Initial allocation for 60 satellites
+ * datesCount: 60, // Initial allocation for 60 timestamps
+ * });
+ * ```
+ *
+ * @throws If calculator dependencies cannot be resolved.
+ * Consult specific calculator type documentation and supply the dependencies.
+ */
+ constructor({ runtime, calculators, satRecsCount, datesCount, }: {
+ runtime: Runtime;
+ calculators: Calculators;
+ satRecsCount: number;
+ datesCount: number;
+ });
+ /**
+ * Sets the satellite records. Can be called between runs to change satellites.
+ * If the provided array is larger than the current allocation, the native array
+ * will be freed and reallocated with the new size.
+ *
+ * @param satRecs - Array of SatRec objects
+ * @throws If the instance is disposed
+ * @throws If a run is currently in progress
+ */
+ setSatRecs(satRecs: SatRec[]): void;
+ /**
+ * Sets the dates for propagation. Can be called between runs to change dates.
+ * If the provided array is larger than the current allocation, the native array
+ * will be freed and reallocated with the new size.
+ *
+ * @param dates - Array of Date objects
+ * @throws If the instance is disposed
+ * @throws If a run is currently in progress
+ */
+ setDates(dates: readonly Date[]): void;
+ private computeTotalOutputSizeBytes;
+ private redistributeOutputBuffer;
+ /**
+ * Executes the bulk propagation for all satellites across all specified dates.
+ * Overwrites previous results since allocalted memory is reused. Returns
+ * `undefined` on calculation completion for single-threaded runtime, or a Promise
+ * for multi-threaded runtime.
+ *
+ * `setSatRecs` and `setDates` must be called before calling `run`.
+ *
+ * @param runParameters - Calculator-specific parameters, keyed by calculator name.
+ * Required (and type checked) if any configured calculator needs parameters
+ * (example: `LookAnglesCalculator` requires observer position); optional otherwise.
+ * Calculators that need no parameters must not be given a key.
+ *
+ * @example
+ * ```typescript
+ * propagator.setSatRecs(satellites);
+ * propagator.setDates(dates);
+ *
+ * // Basic run (no calculator params needed)
+ * propagator.run();
+ *
+ * // Run with calculator parameters
+ * propagator.run({
+ * lookAngles: { observer: {
+ * latitude: degreesToRadians(41),
+ * longitude: degreesToRadians(-71),
+ * height: 0.2,
+ * } },
+ * });
+ * ```
+ *
+ * @throws If the instance is disposed
+ * @throws If setSatRecs or setDates has not been called
+ */
+ run(...[runParameters]: BulkPropagatorRunArgs): Runtime extends MultiThreadRuntime ? Promise : undefined;
+ /**
+ * Retrieves formatted output for a specific satellite at a specific time index.
+ *
+ * @param satelliteIndex - Zero-based index of the satellite (0 to `satRecsCount` - 1)
+ * @param dateIndex - Zero-based index of the date (0 to `datesCount` - 1)
+ * @returns Formatted output object with results from all calculators,
+ * or `undefined` if indices are out of bounds
+ *
+ * @example
+ * ```typescript
+ * // Get results for first satellite at second time point
+ * const result = propagator.getFormattedOutput(0, 1);
+ * if (result && result.eci.error === SatRecError.None) {
+ * const { x, y, z } = result.eci.position;
+ * }
+ *
+ * // Iterate through all results
+ * for (let satIdx = 0; satIdx < satellites.length; satIdx++) {
+ * for (let dateIdx = 0; dateIdx < dates.length; dateIdx++) {
+ * const result = propagator.getFormattedOutput(satIdx, dateIdx)!;
+ * // process results
+ * }
+ * }
+ * ```
+ *
+ * @throws If the instance is disposed
+ */
+ getFormattedOutput(satelliteIndex: number, dateIndex: number): CalculatorsToFormattedOutput | undefined;
+ /**
+ * Retrieves raw output arrays from all calculators. Can be used for further processing.
+ * Bypasses all formatting so should be faster for refinement of all data.
+ *
+ * Raw outputs are typically TypedArrays (Float64Array, etc.) containing all results
+ * in a flattened format. The arrays are views on WebAssembly memory; BulkPropagator overwrites
+ * them during every run.
+ *
+ * @returns Object containing raw output arrays from each calculator.
+ * Each property is named after the calculator.
+ * Each array contains results for all satellites and all dates, sorted first by satellite index,
+ * then by date index:
+ * [satellite 0 date 0, satellite 0 date 1, ... satellite 1 date 0, satellite 1 date 1, ...].
+ *
+ * @example
+ * ```typescript
+ * const rawOutput = propagator.getRawOutput();
+ *
+ * const positions = rawOutput.eci.position; // Float64Array
+ *
+ * // Manual indexing: `positions[satIndex * datesCount * 3 + dateIndex * 3 + component]`
+ * const satIndex = 0, dateIndex = 1;
+ * const x = positions[satIndex * dates.length * 3 + dateIndex * 3 + 0];
+ * const y = positions[satIndex * dates.length * 3 + dateIndex * 3 + 1];
+ * const z = positions[satIndex * dates.length * 3 + dateIndex * 3 + 2];
+ * ```
+ *
+ * @throws If the instance is disposed
+ */
+ getRawOutput(): CalculatorsToRawOutput;
+ private checkIfDisposed;
+ private checkIfRunning;
+ /**
+ * Releases all allocated WebAssembly memory.
+ *
+ * This method is automatically called when using the `using` declaration
+ * (if Symbol.dispose is supported).
+ * Manual disposal is required when not using automatic resource management.
+ *
+ * @example
+ * ```typescript
+ * // Automatic disposal with 'using' (recommended)
+ * using propagator = new BulkPropagator({...});
+ * // disposal happens automatically at the end of the scope
+ *
+ * // Manual disposal
+ * const propagator = new BulkPropagator({...});
+ * propagator.dispose(); // must be called otherwise memory WILL LEAK
+ * ```
+ *
+ * @throws If the instance is disposed already
+ */
+ dispose(): void;
+ [Symbol.dispose]: () => void;
+}
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/bulk-propagator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/bulk-propagator.js
new file mode 100644
index 0000000..be23547
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/bulk-propagator.js
@@ -0,0 +1,426 @@
+import { allocateDatesArray, writeDatesArray } from './date-to-wasm.js';
+import { allocateNativeStructArray, writeNativeStructArrayFromSatrecArray, } from './elsetrec-struct.js';
+import { allocateRunData } from './run-data.js';
+import { topologicalSort } from './toposort.js';
+function ceilToMultipleOf64Bit(bytes) {
+ const bytesPer64Bit = 8;
+ return Math.ceil(bytes / bytesPer64Bit) * bytesPer64Bit;
+}
+/**
+ * This class allows faster propagation of multiple satellites and multiple time points
+ * by batching calculations and leveraging WASM. It supports configurable set of calculators.
+ *
+ * Kitchen sink example to calculate Look Angles:
+ * @example
+ * ```typescript
+ * import {
+ * createWasmModule,
+ * BulkPropagator,
+ * EciBaseCalculator, EcfPositionCalculator, GmstCalculator, LookAnglesCalculator
+ * } from 'satellite.js';
+ *
+ * const wasmModule = await createWasmModule();
+ * const satellites = [twoline2satrec(line1, line2)];
+ * const dates = [new Date()];
+ * const observerGeodetic = {
+ * latitude: degreesToRadians(41),
+ * longitude: degreesToRadians(-71),
+ * height: 0.2,
+ * };
+ *
+ * // Important: either use `using` syntax OR call `propagator.dispose()` in the end, otherwise
+ * // your app WILL LEAK MEMORY. Propagator disposal is your responsibility!
+ * using propagator = new BulkPropagator({
+ * wasmModule,
+ * calculators: [
+ * new EciBaseCalculator(), // SGP4 output on which further calculators depend
+ * new GmstCalculator(), // dependency of EcfPosition
+ * new EcfPositionCalculator(), // depends on EciBase and Gmst; dependency of LookAngles
+ * // depends on EcfPosition; requires observer parameter in propagator.run()
+ * new LookAnglesCalculator(),
+ * ],
+ * satRecsCount: satellites.length,
+ * datesCount: dates.length,
+ * });
+ *
+ * propagator.setSatRecs(satellites);
+ * propagator.setDates(dates);
+ * propagator.run({ lookAngles: { observer: observerGeodetic } });
+ *
+ * // Get formatted output for satellite 0 at date 0
+ * const result = propagator.getFormattedOutput(0, 0);
+ * console.log(result?.lookAngles.azimuth); // radians
+ * console.log(result?.lookAngles.elevation); // radians
+ *
+ * // Get raw output in form of TypedArrays for further processing.
+ * // Results are sorted by satellite order and then by date order:
+ * // [satellite 0 date 0, satellite 0 date 1, ..., satellite 1 date 0, satellite 1 date 1, ...]
+ * const rawResult = propagator.getRawOutput();
+ * rawResult.lookAngles; // Float64Array
+ * const [azimuth, elevation, rangeSat] = rawResult.lookAngles;
+ *
+ * // The approach above is the same as pure javascript below,
+ * // but BulkPropagator is much more performant.
+ * const results = [];
+ * for (const satellite of satellites) {
+ * for (const date of dates) {
+ * const result = propagate(satellite, date);
+ * const ecf = eciToEcf(result!.position, gstime(date));
+ * const { azimuth, elevation, rangeSat } = ecfToLookAngles(observerGeodetic, ecf);
+ * }
+ * }
+ * ```
+ *
+ * @template Calculators - Readonly array of Calculator instances
+ */
+export class BulkPropagator {
+ calculators;
+ satrecsPointer;
+ allocatedSatrecsCount;
+ usedSatrecsCount = 0;
+ datesPointer;
+ allocatedDatesCount;
+ usedDatesCount = 0;
+ runtime;
+ runDataPointer;
+ outputPointer;
+ allocatedOutputSizeBytes;
+ outputPointersByCalculator;
+ calculatorDependenciesOutputsPointers;
+ isDisposed = false;
+ isRunning = false;
+ runCompletionPromise = null;
+ needsOutputRedistribution = true;
+ hasSatRecs = false;
+ hasDates = false;
+ /**
+ * Creates a BulkPropagator instance.
+ * The BulkPropagator is generic depending on the Calculator instances passed to it.
+ * The return types of `getFormattedOutput()`, `getRawOutput()`, and argument type of `run()`
+ * depend on the passed Calculators.
+ *
+ * @param options - Configuration options
+ * @param options.wasmModule - The WebAssembly module instance
+ * (use `createWasmModule()` to create one and reuse it)
+ * @param options.calculators - Array of calculator instances
+ * to run during propagation; they all named as `*Calculator` for easy discovery
+ * @param options.satRecsCount - Initial allocation size for satellite records
+ * @param options.datesCount - Initial allocation size for dates
+ *
+ * @example
+ * ```ts
+ * const propagator = new BulkPropagator({
+ * wasmModule: await createWasmModule(),
+ * calculators: [new EciPositionCalculator()],
+ * satRecsCount: 60, // Initial allocation for 60 satellites
+ * datesCount: 60, // Initial allocation for 60 timestamps
+ * });
+ * ```
+ *
+ * @throws If calculator dependencies cannot be resolved.
+ * Consult specific calculator type documentation and supply the dependencies.
+ */
+ constructor({ runtime, calculators, satRecsCount, datesCount, }) {
+ this.runtime = runtime;
+ if (Symbol.dispose) {
+ this[Symbol.dispose] = () => this.dispose();
+ }
+ this.satrecsPointer = allocateNativeStructArray(runtime.module, satRecsCount);
+ this.allocatedSatrecsCount = satRecsCount;
+ this.datesPointer = allocateDatesArray(runtime.module, datesCount);
+ this.allocatedDatesCount = datesCount;
+ const sorted = topologicalSort(calculators.map((calculator) => ({
+ provides: calculator.name,
+ hasDependencies: calculator.dependencies,
+ })));
+ this.calculators = sorted.map((name) => {
+ const calculator = calculators.find((calc) => calc.name === name);
+ return calculator;
+ });
+ this.runDataPointer = allocateRunData(runtime.module);
+ this.allocatedOutputSizeBytes = this.computeTotalOutputSizeBytes(satRecsCount, datesCount);
+ this.outputPointer = runtime.module._malloc(this.allocatedOutputSizeBytes);
+ this.outputPointersByCalculator = new Map();
+ this.calculatorDependenciesOutputsPointers = new Map();
+ }
+ /**
+ * Sets the satellite records. Can be called between runs to change satellites.
+ * If the provided array is larger than the current allocation, the native array
+ * will be freed and reallocated with the new size.
+ *
+ * @param satRecs - Array of SatRec objects
+ * @throws If the instance is disposed
+ * @throws If a run is currently in progress
+ */
+ setSatRecs(satRecs) {
+ this.checkIfDisposed();
+ this.checkIfRunning('set satellite records');
+ if (satRecs.length > this.allocatedSatrecsCount) {
+ this.runtime.module._free(this.satrecsPointer);
+ this.satrecsPointer = allocateNativeStructArray(this.runtime.module, satRecs.length);
+ this.allocatedSatrecsCount = satRecs.length;
+ }
+ writeNativeStructArrayFromSatrecArray(this.runtime.module, this.satrecsPointer, satRecs);
+ if (satRecs.length !== this.usedSatrecsCount) {
+ this.needsOutputRedistribution = true;
+ }
+ this.usedSatrecsCount = satRecs.length;
+ this.hasSatRecs = true;
+ }
+ /**
+ * Sets the dates for propagation. Can be called between runs to change dates.
+ * If the provided array is larger than the current allocation, the native array
+ * will be freed and reallocated with the new size.
+ *
+ * @param dates - Array of Date objects
+ * @throws If the instance is disposed
+ * @throws If a run is currently in progress
+ */
+ setDates(dates) {
+ this.checkIfDisposed();
+ this.checkIfRunning('set dates');
+ if (dates.length > this.allocatedDatesCount) {
+ this.runtime.module._free(this.datesPointer);
+ this.datesPointer = allocateDatesArray(this.runtime.module, dates.length);
+ this.allocatedDatesCount = dates.length;
+ }
+ writeDatesArray(this.runtime.module, this.datesPointer, dates);
+ if (dates.length !== this.usedDatesCount) {
+ this.needsOutputRedistribution = true;
+ }
+ this.usedDatesCount = dates.length;
+ this.hasDates = true;
+ }
+ computeTotalOutputSizeBytes(satRecsCount, datesCount) {
+ let totalBytes = 0;
+ for (const calculator of this.calculators) {
+ totalBytes += ceilToMultipleOf64Bit(calculator.getOutputBufferSize(satRecsCount, datesCount));
+ }
+ return totalBytes;
+ }
+ redistributeOutputBuffer() {
+ const requiredBytes = this.computeTotalOutputSizeBytes(this.usedSatrecsCount, this.usedDatesCount);
+ if (requiredBytes > this.allocatedOutputSizeBytes) {
+ this.runtime.module._free(this.outputPointer);
+ this.outputPointer = this.runtime.module._malloc(requiredBytes);
+ this.allocatedOutputSizeBytes = requiredBytes;
+ }
+ let offsetBytes = 0;
+ this.outputPointersByCalculator = new Map();
+ for (const calculator of this.calculators) {
+ const sizeBytes = ceilToMultipleOf64Bit(calculator.getOutputBufferSize(this.usedSatrecsCount, this.usedDatesCount));
+ this.outputPointersByCalculator.set(calculator.name, this.outputPointer + offsetBytes);
+ offsetBytes += sizeBytes;
+ }
+ this.calculatorDependenciesOutputsPointers = new Map();
+ for (const calculator of this.calculators) {
+ const dependenciesPointers = calculator.dependencies.map(
+ // biome-ignore lint/style/noNonNullAssertion: set for every calculator in the loop above
+ (dependency) => this.outputPointersByCalculator.get(dependency));
+ this.calculatorDependenciesOutputsPointers.set(calculator.name, dependenciesPointers);
+ calculator.init(this.runtime.module,
+ // biome-ignore lint/style/noNonNullAssertion: set for every calculator in the loop above
+ this.outputPointersByCalculator.get(calculator.name), this.usedSatrecsCount, this.usedDatesCount);
+ }
+ this.needsOutputRedistribution = false;
+ }
+ /**
+ * Executes the bulk propagation for all satellites across all specified dates.
+ * Overwrites previous results since allocalted memory is reused. Returns
+ * `undefined` on calculation completion for single-threaded runtime, or a Promise
+ * for multi-threaded runtime.
+ *
+ * `setSatRecs` and `setDates` must be called before calling `run`.
+ *
+ * @param runParameters - Calculator-specific parameters, keyed by calculator name.
+ * Required (and type checked) if any configured calculator needs parameters
+ * (example: `LookAnglesCalculator` requires observer position); optional otherwise.
+ * Calculators that need no parameters must not be given a key.
+ *
+ * @example
+ * ```typescript
+ * propagator.setSatRecs(satellites);
+ * propagator.setDates(dates);
+ *
+ * // Basic run (no calculator params needed)
+ * propagator.run();
+ *
+ * // Run with calculator parameters
+ * propagator.run({
+ * lookAngles: { observer: {
+ * latitude: degreesToRadians(41),
+ * longitude: degreesToRadians(-71),
+ * height: 0.2,
+ * } },
+ * });
+ * ```
+ *
+ * @throws If the instance is disposed
+ * @throws If setSatRecs or setDates has not been called
+ */
+ run(...[runParameters]) {
+ this.checkIfDisposed();
+ if (!this.hasSatRecs) {
+ throw new Error('setSatRecs() must be called before run()');
+ }
+ if (!this.hasDates) {
+ throw new Error('setDates() must be called before run()');
+ }
+ if (this.needsOutputRedistribution) {
+ this.redistributeOutputBuffer();
+ }
+ // `runParameters` is keyed by calculator name, but only for calculators that
+ // actually require parameters; the rest read `undefined` and ignore it.
+ const runParametersByName = (runParameters ?? {});
+ const runDataItems = this.calculators.map((calculator) =>
+ // Calculators whose run parameters are empty get `{}` and ignore it; the
+ // ones that need parameters are guaranteed a key by `BulkPropagatorRunArgs`.
+ calculator.getExecutionDescriptor(runParametersByName[calculator.name] ?? {}));
+ const runData = Object.assign({
+ satellitesPointer: this.satrecsPointer,
+ satellitesCount: this.usedSatrecsCount,
+ jdaysPointer: this.datesPointer,
+ jdaysCount: this.usedDatesCount,
+ }, ...runDataItems);
+ this.isRunning = true;
+ const result = this.runtime.compute(runData, this.runDataPointer);
+ if (result instanceof Promise) {
+ this.runCompletionPromise = result.finally(() => {
+ this.isRunning = false;
+ this.runCompletionPromise = null;
+ });
+ }
+ else {
+ this.isRunning = false;
+ }
+ // `compute` returns `void` for the single-threaded runtime, which is always
+ // `undefined` at runtime.
+ return result;
+ }
+ /**
+ * Retrieves formatted output for a specific satellite at a specific time index.
+ *
+ * @param satelliteIndex - Zero-based index of the satellite (0 to `satRecsCount` - 1)
+ * @param dateIndex - Zero-based index of the date (0 to `datesCount` - 1)
+ * @returns Formatted output object with results from all calculators,
+ * or `undefined` if indices are out of bounds
+ *
+ * @example
+ * ```typescript
+ * // Get results for first satellite at second time point
+ * const result = propagator.getFormattedOutput(0, 1);
+ * if (result && result.eci.error === SatRecError.None) {
+ * const { x, y, z } = result.eci.position;
+ * }
+ *
+ * // Iterate through all results
+ * for (let satIdx = 0; satIdx < satellites.length; satIdx++) {
+ * for (let dateIdx = 0; dateIdx < dates.length; dateIdx++) {
+ * const result = propagator.getFormattedOutput(satIdx, dateIdx)!;
+ * // process results
+ * }
+ * }
+ * ```
+ *
+ * @throws If the instance is disposed
+ */
+ getFormattedOutput(satelliteIndex, dateIndex) {
+ this.checkIfDisposed();
+ if (satelliteIndex >= this.usedSatrecsCount ||
+ dateIndex >= this.usedDatesCount) {
+ return undefined;
+ }
+ const result = {};
+ for (const calculator of this.calculators) {
+ const output = calculator.getFormattedOutput(satelliteIndex, dateIndex);
+ result[calculator.name] = output;
+ }
+ return result;
+ }
+ /**
+ * Retrieves raw output arrays from all calculators. Can be used for further processing.
+ * Bypasses all formatting so should be faster for refinement of all data.
+ *
+ * Raw outputs are typically TypedArrays (Float64Array, etc.) containing all results
+ * in a flattened format. The arrays are views on WebAssembly memory; BulkPropagator overwrites
+ * them during every run.
+ *
+ * @returns Object containing raw output arrays from each calculator.
+ * Each property is named after the calculator.
+ * Each array contains results for all satellites and all dates, sorted first by satellite index,
+ * then by date index:
+ * [satellite 0 date 0, satellite 0 date 1, ... satellite 1 date 0, satellite 1 date 1, ...].
+ *
+ * @example
+ * ```typescript
+ * const rawOutput = propagator.getRawOutput();
+ *
+ * const positions = rawOutput.eci.position; // Float64Array
+ *
+ * // Manual indexing: `positions[satIndex * datesCount * 3 + dateIndex * 3 + component]`
+ * const satIndex = 0, dateIndex = 1;
+ * const x = positions[satIndex * dates.length * 3 + dateIndex * 3 + 0];
+ * const y = positions[satIndex * dates.length * 3 + dateIndex * 3 + 1];
+ * const z = positions[satIndex * dates.length * 3 + dateIndex * 3 + 2];
+ * ```
+ *
+ * @throws If the instance is disposed
+ */
+ getRawOutput() {
+ this.checkIfDisposed();
+ const result = {};
+ for (const calculator of this.calculators) {
+ const output = calculator.getRawOutput();
+ result[calculator.name] = output;
+ }
+ return result;
+ }
+ checkIfDisposed() {
+ if (this.isDisposed) {
+ throw new Error('This BulkPropagator instance is disposed and its memory freed; construct a new one, or check `using` scope or `dispose()` call');
+ }
+ }
+ checkIfRunning(action) {
+ if (this.isRunning) {
+ throw new Error(`Cannot ${action} while a run is in progress`);
+ }
+ }
+ /**
+ * Releases all allocated WebAssembly memory.
+ *
+ * This method is automatically called when using the `using` declaration
+ * (if Symbol.dispose is supported).
+ * Manual disposal is required when not using automatic resource management.
+ *
+ * @example
+ * ```typescript
+ * // Automatic disposal with 'using' (recommended)
+ * using propagator = new BulkPropagator({...});
+ * // disposal happens automatically at the end of the scope
+ *
+ * // Manual disposal
+ * const propagator = new BulkPropagator({...});
+ * propagator.dispose(); // must be called otherwise memory WILL LEAK
+ * ```
+ *
+ * @throws If the instance is disposed already
+ */
+ dispose() {
+ if (this.isDisposed)
+ return;
+ this.isDisposed = true;
+ const freeMemory = () => {
+ this.runtime.module._free(this.satrecsPointer);
+ this.runtime.module._free(this.datesPointer);
+ this.runtime.module._free(this.runDataPointer);
+ this.runtime.module._free(this.outputPointer);
+ };
+ if (this.runCompletionPromise) {
+ this.runCompletionPromise.finally(freeMemory);
+ }
+ else {
+ freeMemory();
+ }
+ }
+ [Symbol.dispose];
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/calculator-interface.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/calculator-interface.d.ts
new file mode 100644
index 0000000..6377056
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/calculator-interface.d.ts
@@ -0,0 +1,13 @@
+import type { RunData } from '../run-data.js';
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { TypedArray } from '../typed-array.js';
+import type { TupleOf } from './tuple-of.js';
+export interface Calculator, RawOutputs extends TypedArray | Record, FormattedOutput = unknown, RunParameters extends Record = Record, ExecutionDescriptor extends Partial = Partial> {
+ readonly name: Name;
+ readonly dependencies: Dependencies;
+ init(module: WasmModuleBase, outputPointer: number, satellitesCount: number, datesCount: number): void;
+ getFormattedOutput(satelliteIndex: number, dateIndex: number): FormattedOutput;
+ getOutputBufferSize(satellitesCount: number, datesCount: number): number;
+ getRawOutput(): RawOutputs;
+ getExecutionDescriptor(runParameters: RunParameters): ExecutionDescriptor;
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/calculator-interface.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/calculator-interface.js
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/calculator-interface.js
@@ -0,0 +1 @@
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/doppler-factor-calculator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/doppler-factor-calculator.d.ts
new file mode 100644
index 0000000..4351342
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/doppler-factor-calculator.d.ts
@@ -0,0 +1,41 @@
+import type { EcfVec3, Kilometer } from '../../common-types.js';
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { Calculator } from './calculator-interface.js';
+/**
+ * Calculator for Doppler factor.
+ *
+ * Depends on:
+ * @see EcfPositionCalculator
+ * @see EcfVelocityCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [dopplerFactor0, dopplerFactor1, ...] for each satellite/date pair
+ *
+ * Provides formatted output as a number under `dopplerFactor` property.
+ */
+export declare class DopplerFactorCalculator implements Calculator<'dopplerFactor', 2, [
+ 'ecfPosition',
+ 'ecfVelocity'
+], Float64Array, number, {
+ observer: EcfVec3;
+}> {
+ readonly name = "dopplerFactor";
+ readonly dependencies: ['ecfPosition', 'ecfVelocity'];
+ private satellitesCount;
+ private datesCount;
+ private module;
+ private outputPointer;
+ init(module: WasmModuleBase, outputPointer: number, satellitesCount: number, datesCount: number): void;
+ getFormattedOutput(satelliteIndex: number, dateIndex: number): number;
+ getRawOutput(): Float64Array;
+ getOutputBufferSize(satellitesCount: number, datesCount: number): number;
+ getExecutionDescriptor(runParameters: {
+ observer: EcfVec3;
+ }): {
+ dopplerFactorEnabled: boolean;
+ observerEcfX: number;
+ observerEcfY: number;
+ observerEcfZ: number;
+ dopplerFactors: number;
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/doppler-factor-calculator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/doppler-factor-calculator.js
new file mode 100644
index 0000000..1c27987
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/doppler-factor-calculator.js
@@ -0,0 +1,50 @@
+/**
+ * Calculator for Doppler factor.
+ *
+ * Depends on:
+ * @see EcfPositionCalculator
+ * @see EcfVelocityCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [dopplerFactor0, dopplerFactor1, ...] for each satellite/date pair
+ *
+ * Provides formatted output as a number under `dopplerFactor` property.
+ */
+export class DopplerFactorCalculator {
+ name = 'dopplerFactor';
+ dependencies = [
+ 'ecfPosition',
+ 'ecfVelocity',
+ ];
+ satellitesCount;
+ datesCount;
+ module;
+ outputPointer;
+ init(module, outputPointer, satellitesCount, datesCount) {
+ this.module = module;
+ this.outputPointer = outputPointer;
+ this.satellitesCount = satellitesCount;
+ this.datesCount = datesCount;
+ }
+ getFormattedOutput(satelliteIndex, dateIndex) {
+ const rawOutput = this.getRawOutput();
+ const index = satelliteIndex * this.datesCount + dateIndex;
+ // biome-ignore lint/style/noNonNullAssertion: index math
+ return rawOutput[index];
+ }
+ getRawOutput() {
+ return new Float64Array(this.module.HEAP8.buffer, this.outputPointer, this.satellitesCount * this.datesCount);
+ }
+ getOutputBufferSize(satellitesCount, datesCount) {
+ return satellitesCount * datesCount * Float64Array.BYTES_PER_ELEMENT;
+ }
+ getExecutionDescriptor(runParameters) {
+ return {
+ dopplerFactorEnabled: true,
+ observerEcfX: runParameters.observer.x,
+ observerEcfY: runParameters.observer.y,
+ observerEcfZ: runParameters.observer.z,
+ dopplerFactors: this.outputPointer,
+ };
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-position-calculator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-position-calculator.d.ts
new file mode 100644
index 0000000..6d8d12d
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-position-calculator.d.ts
@@ -0,0 +1,34 @@
+import type { EcfVec3, Kilometer } from '../../index.js';
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { Calculator } from './calculator-interface.js';
+/**
+ * Calculator for ECF (Earth-Centered Fixed) position.
+ *
+ * Depends on:
+ * @see EciBaseCalculator
+ * @see GmstCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [x0, y0, z0, x1, y1, z1, ...] for each satellite/date pair
+ *
+ * Provides formatted output under `ecfPosition` property.
+ */
+export declare class EcfPositionCalculator implements Calculator<'ecfPosition', 2, [
+ 'eci',
+ 'gmst'
+], Float64Array, EcfVec3> {
+ readonly name = "ecfPosition";
+ readonly dependencies: ['eci', 'gmst'];
+ private satellitesCount;
+ private datesCount;
+ private module;
+ private outputPointer;
+ init(module: WasmModuleBase, outputPointer: number, satellitesCount: number, datesCount: number): void;
+ getFormattedOutput(satelliteIndex: number, dateIndex: number): EcfVec3;
+ getOutputBufferSize(satellitesCount: number, datesCount: number): number;
+ getRawOutput(): Float64Array;
+ getExecutionDescriptor(): {
+ ecfPositionEnabled: boolean;
+ ecfPositions: number;
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-position-calculator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-position-calculator.js
new file mode 100644
index 0000000..90a3089
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-position-calculator.js
@@ -0,0 +1,50 @@
+const DIMENSIONS = 3;
+/**
+ * Calculator for ECF (Earth-Centered Fixed) position.
+ *
+ * Depends on:
+ * @see EciBaseCalculator
+ * @see GmstCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [x0, y0, z0, x1, y1, z1, ...] for each satellite/date pair
+ *
+ * Provides formatted output under `ecfPosition` property.
+ */
+export class EcfPositionCalculator {
+ name = 'ecfPosition';
+ dependencies = ['eci', 'gmst'];
+ satellitesCount;
+ datesCount;
+ module;
+ outputPointer;
+ init(module, outputPointer, satellitesCount, datesCount) {
+ this.module = module;
+ this.outputPointer = outputPointer;
+ this.satellitesCount = satellitesCount;
+ this.datesCount = datesCount;
+ }
+ getFormattedOutput(satelliteIndex, dateIndex) {
+ const rawOutput = this.getRawOutput();
+ const index = (satelliteIndex * this.datesCount + dateIndex) * DIMENSIONS;
+ return {
+ // biome-ignore-start lint/style/noNonNullAssertion: index math
+ x: rawOutput[index],
+ y: rawOutput[index + 1],
+ z: rawOutput[index + 2],
+ // biome-ignore-end lint/style/noNonNullAssertion: index math
+ };
+ }
+ getOutputBufferSize(satellitesCount, datesCount) {
+ return (satellitesCount * datesCount * DIMENSIONS * Float64Array.BYTES_PER_ELEMENT);
+ }
+ getRawOutput() {
+ return new Float64Array(this.module.HEAP8.buffer, this.outputPointer, this.satellitesCount * this.datesCount * DIMENSIONS);
+ }
+ getExecutionDescriptor() {
+ return {
+ ecfPositionEnabled: true,
+ ecfPositions: this.outputPointer,
+ };
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-velocity-calculator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-velocity-calculator.d.ts
new file mode 100644
index 0000000..62db21f
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-velocity-calculator.d.ts
@@ -0,0 +1,34 @@
+import type { EcfVec3, KilometerPerSecond } from '../../common-types.js';
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { Calculator } from './calculator-interface.js';
+/**
+ * Calculator for ECF (Earth-Centered Fixed) velocity.
+ *
+ * Depends on:
+ * @see EciBaseCalculator
+ * @see GmstCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [vx0, vy0, vz0, vx1, vy1, vz1, ...] for each satellite/date pair
+ *
+ * Provides formatted output under `ecfVelocity` property.
+ */
+export declare class EcfVelocityCalculator implements Calculator<'ecfVelocity', 2, [
+ 'eci',
+ 'gmst'
+], Float64Array, EcfVec3> {
+ readonly name = "ecfVelocity";
+ readonly dependencies: ['eci', 'gmst'];
+ private satellitesCount;
+ private datesCount;
+ private module;
+ private outputPointer;
+ init(module: WasmModuleBase, outputPointer: number, satellitesCount: number, datesCount: number): void;
+ getFormattedOutput(satelliteIndex: number, dateIndex: number): EcfVec3;
+ getOutputBufferSize(satellitesCount: number, datesCount: number): number;
+ getRawOutput(): Float64Array;
+ getExecutionDescriptor(): {
+ ecfVelocityEnabled: boolean;
+ ecfVelocities: number;
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-velocity-calculator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-velocity-calculator.js
new file mode 100644
index 0000000..9c2bf2e
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/ecf-velocity-calculator.js
@@ -0,0 +1,50 @@
+const DIMENSIONS = 3;
+/**
+ * Calculator for ECF (Earth-Centered Fixed) velocity.
+ *
+ * Depends on:
+ * @see EciBaseCalculator
+ * @see GmstCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [vx0, vy0, vz0, vx1, vy1, vz1, ...] for each satellite/date pair
+ *
+ * Provides formatted output under `ecfVelocity` property.
+ */
+export class EcfVelocityCalculator {
+ name = 'ecfVelocity';
+ dependencies = ['eci', 'gmst'];
+ satellitesCount;
+ datesCount;
+ module;
+ outputPointer;
+ init(module, outputPointer, satellitesCount, datesCount) {
+ this.module = module;
+ this.outputPointer = outputPointer;
+ this.satellitesCount = satellitesCount;
+ this.datesCount = datesCount;
+ }
+ getFormattedOutput(satelliteIndex, dateIndex) {
+ const rawOutput = this.getRawOutput();
+ const index = (satelliteIndex * this.datesCount + dateIndex) * DIMENSIONS;
+ return {
+ // biome-ignore-start lint/style/noNonNullAssertion: index math
+ x: rawOutput[index],
+ y: rawOutput[index + 1],
+ z: rawOutput[index + 2],
+ // biome-ignore-end lint/style/noNonNullAssertion: index math
+ };
+ }
+ getOutputBufferSize(satellitesCount, datesCount) {
+ return (satellitesCount * datesCount * DIMENSIONS * Float64Array.BYTES_PER_ELEMENT);
+ }
+ getRawOutput() {
+ return new Float64Array(this.module.HEAP8.buffer, this.outputPointer, this.satellitesCount * this.datesCount * DIMENSIONS);
+ }
+ getExecutionDescriptor() {
+ return {
+ ecfVelocityEnabled: true,
+ ecfVelocities: this.outputPointer,
+ };
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/eci-base-calculator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/eci-base-calculator.d.ts
new file mode 100644
index 0000000..9b42b99
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/eci-base-calculator.d.ts
@@ -0,0 +1,62 @@
+import type { EciVec3, Kilometer, KilometerPerSecond } from '../../common-types.js';
+import type { SatRecError } from '../../propagation/SatRec.js';
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { Calculator } from './calculator-interface.js';
+export interface EciBaseFormattedOutput {
+ position: EciVec3;
+ velocity: EciVec3;
+ error: SatRecError;
+}
+export type EciBaseRunParameters = {
+ /**
+ * Opt in to the community fix for satellites that decayed long ago, which SGP4
+ * would otherwise propagate to meaningless positions instead of reporting as
+ * decayed. If true, will return SatRecError.Decayed on those satellites.
+ *
+ * This check is NOT part of the official SGP4 algotithm, so if you need completely compliant
+ * SGP4 output *despite* it sometimes giving garbage positions for decayed satellites,
+ * you should NOT use it.
+ */
+ communityDecayCheckEnabled?: boolean;
+};
+/**
+ * Performs SGP4 propagation, producing ECI (Earth-Centered Fixed) position
+ * and velocity vectors. Base calculator with no dependencies.
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `position`: `Float64Array`, packed as [x0, y0, z0, x1, y1, z1, ...]
+ * for each satellite/date pair
+ * - `velocity`: `Float64Array`, packed as [vx0, vy0, vz0, vx1, vy1, vz1, ...]
+ * for each satellite/date pair
+ * - `error`: `Int8Array`, equal to SatRec.error, packed as [err0, err1, ...]
+ * for each satellite/date pair
+ *
+ * Provides formatted output under `eci` property, @see EciBaseFormattedOutput.
+ */
+export declare class EciBaseCalculator implements Calculator<'eci', 0, [
+], {
+ position: Float64Array;
+ velocity: Float64Array;
+ error: Int8Array;
+}, EciBaseFormattedOutput, EciBaseRunParameters> {
+ readonly name = "eci";
+ readonly dependencies: [];
+ private satellitesCount;
+ private datesCount;
+ private module;
+ private outputPointer;
+ init(module: WasmModuleBase, outputPointer: number, satellitesCount: number, datesCount: number): void;
+ getFormattedOutput(satelliteIndex: number, dateIndex: number): EciBaseFormattedOutput;
+ getRawOutput(): {
+ position: Float64Array;
+ velocity: Float64Array;
+ error: Int8Array;
+ };
+ getOutputBufferSize(satellitesCount: number, datesCount: number): number;
+ getExecutionDescriptor(runParameters: EciBaseRunParameters): {
+ communityDecayCheckEnabled: boolean;
+ eciPositions: number;
+ eciVelocities: number;
+ sgp4Errors: number;
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/eci-base-calculator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/eci-base-calculator.js
new file mode 100644
index 0000000..6bb37b5
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/eci-base-calculator.js
@@ -0,0 +1,75 @@
+const DIMENSIONS = 3;
+const BYTES_PER_VECTOR = DIMENSIONS * Float64Array.BYTES_PER_ELEMENT;
+/**
+ * Performs SGP4 propagation, producing ECI (Earth-Centered Fixed) position
+ * and velocity vectors. Base calculator with no dependencies.
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `position`: `Float64Array`, packed as [x0, y0, z0, x1, y1, z1, ...]
+ * for each satellite/date pair
+ * - `velocity`: `Float64Array`, packed as [vx0, vy0, vz0, vx1, vy1, vz1, ...]
+ * for each satellite/date pair
+ * - `error`: `Int8Array`, equal to SatRec.error, packed as [err0, err1, ...]
+ * for each satellite/date pair
+ *
+ * Provides formatted output under `eci` property, @see EciBaseFormattedOutput.
+ */
+export class EciBaseCalculator {
+ name = 'eci';
+ dependencies = [];
+ satellitesCount;
+ datesCount;
+ module;
+ outputPointer;
+ init(module, outputPointer, satellitesCount, datesCount) {
+ this.module = module;
+ this.outputPointer = outputPointer;
+ this.satellitesCount = satellitesCount;
+ this.datesCount = datesCount;
+ }
+ getFormattedOutput(satelliteIndex, dateIndex) {
+ const { position, velocity, error } = this.getRawOutput();
+ const index = (satelliteIndex * this.datesCount + dateIndex) * DIMENSIONS;
+ return {
+ // biome-ignore-start lint/style/noNonNullAssertion: index math
+ position: {
+ x: position[index],
+ y: position[index + 1],
+ z: position[index + 2],
+ },
+ velocity: {
+ x: velocity[index],
+ y: velocity[index + 1],
+ z: velocity[index + 2],
+ },
+ // biome-ignore-end lint/style/noNonNullAssertion: index math
+ error: error[satelliteIndex * this.datesCount + dateIndex],
+ };
+ }
+ getRawOutput() {
+ const vectorsSize = this.satellitesCount * this.datesCount * DIMENSIONS;
+ const position = new Float64Array(this.module.HEAP8.buffer, this.outputPointer, vectorsSize);
+ const velocity = new Float64Array(this.module.HEAP8.buffer, position.byteOffset + position.byteLength, vectorsSize);
+ const error = new Int8Array(this.module.HEAP8.buffer, velocity.byteOffset + velocity.byteLength, this.satellitesCount * this.datesCount);
+ return {
+ position,
+ velocity,
+ error,
+ };
+ }
+ getOutputBufferSize(satellitesCount, datesCount) {
+ return (BYTES_PER_VECTOR * satellitesCount * datesCount * 2 +
+ Int8Array.BYTES_PER_ELEMENT * satellitesCount * datesCount);
+ }
+ getExecutionDescriptor(runParameters) {
+ const eciVelocitiesPointer = this.outputPointer +
+ BYTES_PER_VECTOR * this.satellitesCount * this.datesCount;
+ return {
+ communityDecayCheckEnabled: runParameters.communityDecayCheckEnabled ?? false,
+ eciPositions: this.outputPointer,
+ eciVelocities: eciVelocitiesPointer,
+ sgp4Errors: eciVelocitiesPointer +
+ BYTES_PER_VECTOR * this.satellitesCount * this.datesCount,
+ };
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/geodetic-position-calculator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/geodetic-position-calculator.d.ts
new file mode 100644
index 0000000..0d9ee29
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/geodetic-position-calculator.d.ts
@@ -0,0 +1,36 @@
+import type { GeodeticLocation } from '../../common-types.js';
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { Calculator } from './calculator-interface.js';
+/**
+ * Calculator for Geodetic position (latitude, longitude, height).
+ *
+ * Depends on:
+ * @see EciBaseCalculator
+ * @see GmstCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [lat0, lon0, height0, lat1, lon1, height1, ...]
+ * for each satellite/date pair
+ *
+ * Provides formatted output under `geodeticPosition` property,
+ * @see GeodeticPositionFormattedOutput.
+ */
+export declare class GeodeticPositionCalculator implements Calculator<'geodeticPosition', 2, [
+ 'eci',
+ 'gmst'
+], Float64Array, GeodeticLocation> {
+ readonly name = "geodeticPosition";
+ readonly dependencies: ['eci', 'gmst'];
+ private satellitesCount;
+ private datesCount;
+ private module;
+ private outputPointer;
+ init(module: WasmModuleBase, outputPointer: number, satellitesCount: number, datesCount: number): void;
+ getFormattedOutput(satelliteIndex: number, dateIndex: number): GeodeticLocation;
+ getOutputBufferSize(satellitesCount: number, datesCount: number): number;
+ getRawOutput(): Float64Array;
+ getExecutionDescriptor(): {
+ geodeticPositionEnabled: boolean;
+ geodeticPositions: number;
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/geodetic-position-calculator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/geodetic-position-calculator.js
new file mode 100644
index 0000000..c8e17cf
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/geodetic-position-calculator.js
@@ -0,0 +1,52 @@
+const DIMENSIONS = 3;
+/**
+ * Calculator for Geodetic position (latitude, longitude, height).
+ *
+ * Depends on:
+ * @see EciBaseCalculator
+ * @see GmstCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [lat0, lon0, height0, lat1, lon1, height1, ...]
+ * for each satellite/date pair
+ *
+ * Provides formatted output under `geodeticPosition` property,
+ * @see GeodeticPositionFormattedOutput.
+ */
+export class GeodeticPositionCalculator {
+ name = 'geodeticPosition';
+ dependencies = ['eci', 'gmst'];
+ satellitesCount;
+ datesCount;
+ module;
+ outputPointer;
+ init(module, outputPointer, satellitesCount, datesCount) {
+ this.module = module;
+ this.outputPointer = outputPointer;
+ this.satellitesCount = satellitesCount;
+ this.datesCount = datesCount;
+ }
+ getFormattedOutput(satelliteIndex, dateIndex) {
+ const rawOutput = this.getRawOutput();
+ const index = (satelliteIndex * this.datesCount + dateIndex) * DIMENSIONS;
+ return {
+ // biome-ignore-start lint/style/noNonNullAssertion: index math
+ latitude: rawOutput[index],
+ longitude: rawOutput[index + 1],
+ height: rawOutput[index + 2],
+ // biome-ignore-end lint/style/noNonNullAssertion: index math
+ };
+ }
+ getOutputBufferSize(satellitesCount, datesCount) {
+ return (satellitesCount * datesCount * DIMENSIONS * Float64Array.BYTES_PER_ELEMENT);
+ }
+ getRawOutput() {
+ return new Float64Array(this.module.HEAP8.buffer, this.outputPointer, this.satellitesCount * this.datesCount * DIMENSIONS);
+ }
+ getExecutionDescriptor() {
+ return {
+ geodeticPositionEnabled: true,
+ geodeticPositions: this.outputPointer,
+ };
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/gmst-calculator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/gmst-calculator.d.ts
new file mode 100644
index 0000000..1ba7f39
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/gmst-calculator.d.ts
@@ -0,0 +1,26 @@
+import type { GMSTime } from '../../common-types.js';
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { Calculator } from './calculator-interface.js';
+/**
+ * Calculator for GMST (Greenwich Mean Sidereal Time), required for many coordinate transforms.
+ *
+ * Raw output format:
+ * - `Float64Array`, packed as [gmst0, gmst1, ...] for each date. Not duplicated per satellite.
+ *
+ * Provides formatted output as a number under `gmst` property.
+ */
+export declare class GmstCalculator implements Calculator<'gmst', 0, [], Float64Array, GMSTime> {
+ readonly name = "gmst";
+ readonly dependencies: [];
+ private module;
+ private outputPointer;
+ private datesCount;
+ getOutputBufferSize(_satellitesCount: number, datesCount: number): number;
+ init(module: WasmModuleBase, outputPointer: number, _satellitesCount: number, datesCount: number): void;
+ getRawOutput(): Float64Array;
+ getFormattedOutput(_satelliteIndex: number, dateIndex: number): GMSTime;
+ getExecutionDescriptor(): {
+ gmstEnabled: boolean;
+ gmstValues: number;
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/gmst-calculator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/gmst-calculator.js
new file mode 100644
index 0000000..02a2a7f
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/gmst-calculator.js
@@ -0,0 +1,36 @@
+/**
+ * Calculator for GMST (Greenwich Mean Sidereal Time), required for many coordinate transforms.
+ *
+ * Raw output format:
+ * - `Float64Array`, packed as [gmst0, gmst1, ...] for each date. Not duplicated per satellite.
+ *
+ * Provides formatted output as a number under `gmst` property.
+ */
+export class GmstCalculator {
+ name = 'gmst';
+ dependencies = [];
+ module;
+ outputPointer;
+ datesCount;
+ getOutputBufferSize(_satellitesCount, datesCount) {
+ return datesCount * Float64Array.BYTES_PER_ELEMENT;
+ }
+ init(module, outputPointer, _satellitesCount, datesCount) {
+ this.module = module;
+ this.outputPointer = outputPointer;
+ this.datesCount = datesCount;
+ }
+ getRawOutput() {
+ return new Float64Array(this.module.HEAP8.buffer, this.outputPointer, this.datesCount);
+ }
+ getFormattedOutput(_satelliteIndex, dateIndex) {
+ // biome-ignore lint/style/noNonNullAssertion: index math
+ return this.getRawOutput()[dateIndex];
+ }
+ getExecutionDescriptor() {
+ return {
+ gmstEnabled: true,
+ gmstValues: this.outputPointer,
+ };
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/look-angles-calculator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/look-angles-calculator.d.ts
new file mode 100644
index 0000000..df9402b
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/look-angles-calculator.d.ts
@@ -0,0 +1,58 @@
+import type { GeodeticLocation, LookAngles } from '../../common-types.js';
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { Calculator } from './calculator-interface.js';
+/**
+ * Calculator for Look Angles (azimuth, elevation, rangeSat).
+ *
+ * Requires observer location as a parameter for `BulkPropagator.run()` method.
+ *
+ * Depends on:
+ * @see EcfPositionCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [az0, el0, range0, az1, el1, range1, ...]
+ * for each satellite/date pair
+ *
+ * Provides formatted output under `lookAngles` property.
+ *
+ * @example
+ * ```ts
+ * bulkPropagator.run({
+ * dates: [...],
+ * lookAngles: {
+ * observer: {
+ * latitude: degreesToRadians(41),
+ * longitude: degreesToRadians(-71),
+ * height: 0.1,
+ * }
+ * }
+ * });
+ *
+ * bulkPropagator.getFormattedOutput(satelliteIndex, dateIndex).lookAngles.azimuth;
+ * ```
+ */
+export declare class LookAnglesCalculator implements Calculator<'lookAngles', 1, [
+ 'ecfPosition'
+], Float64Array, LookAngles, {
+ observer: GeodeticLocation;
+}> {
+ readonly name = "lookAngles";
+ readonly dependencies: ['ecfPosition'];
+ private satellitesCount;
+ private datesCount;
+ private module;
+ private outputPointer;
+ init(module: WasmModuleBase, outputPointer: number, satellitesCount: number, datesCount: number): void;
+ getFormattedOutput(satelliteIndex: number, dateIndex: number): LookAngles;
+ getRawOutput(): Float64Array;
+ getOutputBufferSize(satellitesCount: number, datesCount: number): number;
+ getExecutionDescriptor(runParameters: {
+ observer: GeodeticLocation;
+ }): {
+ lookAnglesEnabled: true;
+ latitudeRadians: number;
+ longitudeRadians: number;
+ heightKm: number;
+ lookAngles: number;
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/look-angles-calculator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/look-angles-calculator.js
new file mode 100644
index 0000000..a2e75d9
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/look-angles-calculator.js
@@ -0,0 +1,75 @@
+const OUTPUTS_PER_SATELLITE = 3;
+/**
+ * Calculator for Look Angles (azimuth, elevation, rangeSat).
+ *
+ * Requires observer location as a parameter for `BulkPropagator.run()` method.
+ *
+ * Depends on:
+ * @see EcfPositionCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [az0, el0, range0, az1, el1, range1, ...]
+ * for each satellite/date pair
+ *
+ * Provides formatted output under `lookAngles` property.
+ *
+ * @example
+ * ```ts
+ * bulkPropagator.run({
+ * dates: [...],
+ * lookAngles: {
+ * observer: {
+ * latitude: degreesToRadians(41),
+ * longitude: degreesToRadians(-71),
+ * height: 0.1,
+ * }
+ * }
+ * });
+ *
+ * bulkPropagator.getFormattedOutput(satelliteIndex, dateIndex).lookAngles.azimuth;
+ * ```
+ */
+export class LookAnglesCalculator {
+ name = 'lookAngles';
+ dependencies = ['ecfPosition'];
+ satellitesCount;
+ datesCount;
+ module;
+ outputPointer;
+ init(module, outputPointer, satellitesCount, datesCount) {
+ this.module = module;
+ this.outputPointer = outputPointer;
+ this.satellitesCount = satellitesCount;
+ this.datesCount = datesCount;
+ }
+ getFormattedOutput(satelliteIndex, dateIndex) {
+ const rawOutput = this.getRawOutput();
+ const index = (satelliteIndex * this.datesCount + dateIndex) * OUTPUTS_PER_SATELLITE;
+ return {
+ // biome-ignore-start lint/style/noNonNullAssertion: index math
+ azimuth: rawOutput[index],
+ elevation: rawOutput[index + 1],
+ rangeSat: rawOutput[index + 2],
+ // biome-ignore-end lint/style/noNonNullAssertion: index math
+ };
+ }
+ getRawOutput() {
+ return new Float64Array(this.module.HEAP8.buffer, this.outputPointer, this.satellitesCount * this.datesCount * OUTPUTS_PER_SATELLITE);
+ }
+ getOutputBufferSize(satellitesCount, datesCount) {
+ return (satellitesCount *
+ datesCount *
+ OUTPUTS_PER_SATELLITE *
+ Float64Array.BYTES_PER_ELEMENT);
+ }
+ getExecutionDescriptor(runParameters) {
+ const { latitude, longitude, height } = runParameters.observer;
+ return {
+ lookAnglesEnabled: true,
+ latitudeRadians: latitude,
+ longitudeRadians: longitude,
+ heightKm: height,
+ lookAngles: this.outputPointer,
+ };
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/shadow-fraction-calculator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/shadow-fraction-calculator.d.ts
new file mode 100644
index 0000000..685f3fb
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/shadow-fraction-calculator.d.ts
@@ -0,0 +1,37 @@
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { Calculator } from './calculator-interface.js';
+/**
+ * Calculator for shadow fraction.
+ *
+ * Computes the fraction of the Sun's disc obscured by the Earth as seen from a satellite
+ * at a date. This is the WASM equivalent of the pure-JS `shadowFunction`.
+ *
+ * Depends on:
+ * @see EciBaseCalculator
+ * @see SunPositionCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [shadow0, shadow1, ...] for each satellite/date pair
+ * where 0 = fully lit, 1 = umbra, 0–1 = penumbra fraction
+ *
+ * Provides formatted output as a number under `shadowFraction` property.
+ */
+export declare class ShadowFractionCalculator implements Calculator<'shadowFraction', 2, [
+ 'eci',
+ 'sunPosition'
+], Float64Array, number> {
+ readonly name = "shadowFraction";
+ readonly dependencies: ['eci', 'sunPosition'];
+ private satellitesCount;
+ private datesCount;
+ private module;
+ private outputPointer;
+ init(module: WasmModuleBase, outputPointer: number, satellitesCount: number, datesCount: number): void;
+ getFormattedOutput(satelliteIndex: number, dateIndex: number): number;
+ getRawOutput(): Float64Array;
+ getOutputBufferSize(satellitesCount: number, datesCount: number): number;
+ getExecutionDescriptor(): {
+ shadowFractionEnabled: boolean;
+ shadowFractionValues: number;
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/shadow-fraction-calculator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/shadow-fraction-calculator.js
new file mode 100644
index 0000000..ddc434c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/shadow-fraction-calculator.js
@@ -0,0 +1,48 @@
+/**
+ * Calculator for shadow fraction.
+ *
+ * Computes the fraction of the Sun's disc obscured by the Earth as seen from a satellite
+ * at a date. This is the WASM equivalent of the pure-JS `shadowFunction`.
+ *
+ * Depends on:
+ * @see EciBaseCalculator
+ * @see SunPositionCalculator
+ *
+ * Raw outputs are always sorted by satellite index first, then by date index, and packed as:
+ * - `Float64Array`, packed as [shadow0, shadow1, ...] for each satellite/date pair
+ * where 0 = fully lit, 1 = umbra, 0–1 = penumbra fraction
+ *
+ * Provides formatted output as a number under `shadowFraction` property.
+ */
+export class ShadowFractionCalculator {
+ name = 'shadowFraction';
+ dependencies = ['eci', 'sunPosition'];
+ satellitesCount;
+ datesCount;
+ module;
+ outputPointer;
+ init(module, outputPointer, satellitesCount, datesCount) {
+ this.module = module;
+ this.outputPointer = outputPointer;
+ this.satellitesCount = satellitesCount;
+ this.datesCount = datesCount;
+ }
+ getFormattedOutput(satelliteIndex, dateIndex) {
+ const rawOutput = this.getRawOutput();
+ const index = satelliteIndex * this.datesCount + dateIndex;
+ // biome-ignore lint/style/noNonNullAssertion: index math
+ return rawOutput[index];
+ }
+ getRawOutput() {
+ return new Float64Array(this.module.HEAP8.buffer, this.outputPointer, this.satellitesCount * this.datesCount);
+ }
+ getOutputBufferSize(satellitesCount, datesCount) {
+ return satellitesCount * datesCount * Float64Array.BYTES_PER_ELEMENT;
+ }
+ getExecutionDescriptor() {
+ return {
+ shadowFractionEnabled: true,
+ shadowFractionValues: this.outputPointer,
+ };
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/sun-position-calculator.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/sun-position-calculator.d.ts
new file mode 100644
index 0000000..52237e0
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/sun-position-calculator.d.ts
@@ -0,0 +1,29 @@
+import type { AU, EciVec3 } from '../../index.js';
+import type { WasmModuleBase } from '../runtimes/wasm-module-interfaces.js';
+import type { Calculator } from './calculator-interface.js';
+/**
+ * Calculator for Sun position in AU (equatorial ECI frame).
+ *
+ * Output is per-date only (not per-satellite), since the Sun position depends only
+ * on the date.
+ *
+ * Raw output format:
+ * - `Float64Array`, packed as [x0, y0, z0, x1, y1, z1, ...] for each date.
+ *
+ * Provides formatted output under `sunPosition` property.
+ */
+export declare class SunPositionCalculator implements Calculator<'sunPosition', 0, [], Float64Array, EciVec3> {
+ readonly name = "sunPosition";
+ readonly dependencies: [];
+ private module;
+ private outputPointer;
+ private datesCount;
+ getOutputBufferSize(_satellitesCount: number, datesCount: number): number;
+ init(module: WasmModuleBase, outputPointer: number, _satellitesCount: number, datesCount: number): void;
+ getRawOutput(): Float64Array;
+ getFormattedOutput(_satelliteIndex: number, dateIndex: number): EciVec3;
+ getExecutionDescriptor(): {
+ sunPositionEnabled: boolean;
+ sunPositions: number;
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/sun-position-calculator.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/sun-position-calculator.js
new file mode 100644
index 0000000..b3d4d22
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/sun-position-calculator.js
@@ -0,0 +1,47 @@
+const DIMENSIONS = 3;
+/**
+ * Calculator for Sun position in AU (equatorial ECI frame).
+ *
+ * Output is per-date only (not per-satellite), since the Sun position depends only
+ * on the date.
+ *
+ * Raw output format:
+ * - `Float64Array`, packed as [x0, y0, z0, x1, y1, z1, ...] for each date.
+ *
+ * Provides formatted output under `sunPosition` property.
+ */
+export class SunPositionCalculator {
+ name = 'sunPosition';
+ dependencies = [];
+ module;
+ outputPointer;
+ datesCount;
+ getOutputBufferSize(_satellitesCount, datesCount) {
+ return datesCount * DIMENSIONS * Float64Array.BYTES_PER_ELEMENT;
+ }
+ init(module, outputPointer, _satellitesCount, datesCount) {
+ this.module = module;
+ this.outputPointer = outputPointer;
+ this.datesCount = datesCount;
+ }
+ getRawOutput() {
+ return new Float64Array(this.module.HEAP8.buffer, this.outputPointer, this.datesCount * DIMENSIONS);
+ }
+ getFormattedOutput(_satelliteIndex, dateIndex) {
+ const rawOutput = this.getRawOutput();
+ const index = dateIndex * DIMENSIONS;
+ return {
+ // biome-ignore-start lint/style/noNonNullAssertion: index math
+ x: rawOutput[index],
+ y: rawOutput[index + 1],
+ z: rawOutput[index + 2],
+ // biome-ignore-end lint/style/noNonNullAssertion: index math
+ };
+ }
+ getExecutionDescriptor() {
+ return {
+ sunPositionEnabled: true,
+ sunPositions: this.outputPointer,
+ };
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/tuple-of.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/tuple-of.d.ts
new file mode 100644
index 0000000..67cae00
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/tuple-of.d.ts
@@ -0,0 +1,3 @@
+export type TupleOf = N extends N ? number extends N ? readonly T[] : _TupleOf : never;
+type _TupleOf = R['length'] extends N ? R : _TupleOf;
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/tuple-of.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/tuple-of.js
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/calculators/tuple-of.js
@@ -0,0 +1 @@
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/date-to-wasm.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/date-to-wasm.d.ts
new file mode 100644
index 0000000..5f3a52e
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/date-to-wasm.d.ts
@@ -0,0 +1,3 @@
+import type { WasmModuleBase } from './runtimes/wasm-module-interfaces.js';
+export declare function allocateDatesArray(module: WasmModuleBase, datesCount: number): number;
+export declare function writeDatesArray(module: WasmModuleBase, pointer: number, dates: readonly Date[]): void;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/date-to-wasm.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/date-to-wasm.js
new file mode 100644
index 0000000..944adcc
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/date-to-wasm.js
@@ -0,0 +1,11 @@
+import { jday } from '../ext.js';
+export function allocateDatesArray(module, datesCount) {
+ const pointer = module._malloc(datesCount * Float64Array.BYTES_PER_ELEMENT);
+ return pointer;
+}
+export function writeDatesArray(module, pointer, dates) {
+ const startOffset = pointer / Float64Array.BYTES_PER_ELEMENT;
+ dates.forEach((date, index) => {
+ module.HEAPF64[startOffset + index] = jday(date);
+ });
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/elsetrec-struct.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/elsetrec-struct.d.ts
new file mode 100644
index 0000000..a805867
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/elsetrec-struct.d.ts
@@ -0,0 +1,4 @@
+import type { SatRec } from '../propagation/SatRec.js';
+import type { WasmModuleBase } from './runtimes/wasm-module-interfaces.js';
+export declare function allocateNativeStructArray(module: WasmModuleBase, count: number): number;
+export declare function writeNativeStructArrayFromSatrecArray(module: WasmModuleBase, pointer: number, satrecArray: SatRec[]): void;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/elsetrec-struct.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/elsetrec-struct.js
new file mode 100644
index 0000000..aa81fc7
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/elsetrec-struct.js
@@ -0,0 +1,35 @@
+import * as constants from '../constants.js';
+import { getNativeStructFieldLayout } from './native-structs-from-js.js';
+import { CppMemoryWriter } from './struct-write.js';
+export function allocateNativeStructArray(module, count) {
+ const nativeSize = module._get_elsetrec_size();
+ return module._calloc_one(count * nativeSize);
+}
+export function writeNativeStructArrayFromSatrecArray(module, pointer, satrecArray) {
+ const structLayoutStringPointer = module._create_elsetrec_struct_layout_string_pointer();
+ const layout = getNativeStructFieldLayout(structLayoutStringPointer, module);
+ module._free_struct_layout_string(structLayoutStringPointer);
+ const nativeSize = module._get_elsetrec_size();
+ const writer = new CppMemoryWriter(module.HEAP8.buffer);
+ satrecArray.forEach((satrec, index) => {
+ const currentOffset = index * nativeSize;
+ writer.setBaseOffset(pointer + currentOffset);
+ layout.forEach(({ type, offset, size }, field) => {
+ if (Object.hasOwn(constants, field)) {
+ writer.writeValue(field, offset, type,
+ // biome-ignore lint/performance/noDynamicNamespaceImportAccess: all constants needed here
+ constants[field], size);
+ }
+ if (field === 'no_unkozai') {
+ writer.writeValue(field, offset, type, satrec.no, size);
+ }
+ if (field === 'radiusearthkm') {
+ writer.writeValue(field, offset, type, constants.earthRadius, size);
+ }
+ if (!Object.hasOwn(satrec, field)) {
+ return;
+ }
+ writer.writeValue(field, offset, type, satrec[field], size);
+ });
+ });
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/index.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/index.d.ts
new file mode 100644
index 0000000..18eb372
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/index.d.ts
@@ -0,0 +1,12 @@
+/** biome-ignore-all assist/source/organizeImports: grouped better than by name */
+export { EciBaseCalculator } from './calculators/eci-base-calculator.js';
+export { GmstCalculator } from './calculators/gmst-calculator.js';
+export { EcfPositionCalculator } from './calculators/ecf-position-calculator.js';
+export { EcfVelocityCalculator } from './calculators/ecf-velocity-calculator.js';
+export { GeodeticPositionCalculator } from './calculators/geodetic-position-calculator.js';
+export { LookAnglesCalculator } from './calculators/look-angles-calculator.js';
+export { DopplerFactorCalculator } from './calculators/doppler-factor-calculator.js';
+export { SunPositionCalculator } from './calculators/sun-position-calculator.js';
+export { ShadowFractionCalculator } from './calculators/shadow-fraction-calculator.js';
+export { BulkPropagator } from './bulk-propagator.js';
+export * from './runtimes/index.js';
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/index.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/index.js
new file mode 100644
index 0000000..18eb372
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/index.js
@@ -0,0 +1,12 @@
+/** biome-ignore-all assist/source/organizeImports: grouped better than by name */
+export { EciBaseCalculator } from './calculators/eci-base-calculator.js';
+export { GmstCalculator } from './calculators/gmst-calculator.js';
+export { EcfPositionCalculator } from './calculators/ecf-position-calculator.js';
+export { EcfVelocityCalculator } from './calculators/ecf-velocity-calculator.js';
+export { GeodeticPositionCalculator } from './calculators/geodetic-position-calculator.js';
+export { LookAnglesCalculator } from './calculators/look-angles-calculator.js';
+export { DopplerFactorCalculator } from './calculators/doppler-factor-calculator.js';
+export { SunPositionCalculator } from './calculators/sun-position-calculator.js';
+export { ShadowFractionCalculator } from './calculators/shadow-fraction-calculator.js';
+export { BulkPropagator } from './bulk-propagator.js';
+export * from './runtimes/index.js';
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-field-type.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-field-type.d.ts
new file mode 100644
index 0000000..912c79f
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-field-type.d.ts
@@ -0,0 +1,4 @@
+/**
+ * A union of all types of field which can be present in a C++ struct
+ */
+export type NativeFieldType = 'bool' | 'int' | 'long' | 'double' | 'char[]' | 'char' | 'unsigned char';
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-field-type.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-field-type.js
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-field-type.js
@@ -0,0 +1 @@
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-structs-from-js.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-structs-from-js.d.ts
new file mode 100644
index 0000000..5ec309c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-structs-from-js.d.ts
@@ -0,0 +1,11 @@
+import type { NativeFieldType } from './native-field-type.js';
+import type { WasmModuleBase } from './runtimes/wasm-module-interfaces.js';
+/**
+ * Map of names of fields in C++ `elsetrec` struct, together with their types, offsets and sizes
+ */
+export type NativeStructLayout = Map;
+export declare function getNativeStructFieldLayout(structLayoutStringPointer: number, module: WasmModuleBase): NativeStructLayout;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-structs-from-js.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-structs-from-js.js
new file mode 100644
index 0000000..e146532
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/native-structs-from-js.js
@@ -0,0 +1,8 @@
+export function getNativeStructFieldLayout(structLayoutStringPointer, module) {
+ const structureJsonString = module.UTF8ToString(structLayoutStringPointer);
+ const structureJson = JSON.parse(structureJsonString);
+ return new Map(structureJson.map(([field, type, offset, size]) => [
+ field,
+ { type, offset, size },
+ ]));
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/run-data.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/run-data.d.ts
new file mode 100644
index 0000000..850a581
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/run-data.d.ts
@@ -0,0 +1,36 @@
+import type { NativeStructLayout } from './native-structs-from-js.js';
+import type { WasmModuleBase } from './runtimes/wasm-module-interfaces.js';
+export interface RunData {
+ satellitesPointer: number;
+ satellitesCount: number;
+ jdaysPointer: number;
+ jdaysCount: number;
+ communityDecayCheckEnabled: boolean;
+ eciPositions: number;
+ eciVelocities: number;
+ sgp4Errors: number;
+ gmstEnabled: boolean;
+ ecfPositionEnabled: boolean;
+ ecfVelocityEnabled: boolean;
+ geodeticPositionEnabled: boolean;
+ lookAnglesEnabled: boolean;
+ dopplerFactorEnabled: boolean;
+ gmstValues: number;
+ ecfPositions: number;
+ ecfVelocities: number;
+ geodeticPositions: number;
+ longitudeRadians: number;
+ latitudeRadians: number;
+ heightKm: number;
+ lookAngles: number;
+ observerEcfX: number;
+ observerEcfY: number;
+ observerEcfZ: number;
+ dopplerFactors: number;
+ sunPositionEnabled: boolean;
+ sunPositions: number;
+ shadowFractionEnabled: boolean;
+ shadowFractionValues: number;
+}
+export declare function allocateRunData(module: WasmModuleBase): number;
+export declare function passRunDataToWasm(module: WasmModuleBase, runDataStruct: NativeStructLayout, runData: RunData, runDataPointer: number): number;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/run-data.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/run-data.js
new file mode 100644
index 0000000..686d63e
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/run-data.js
@@ -0,0 +1,16 @@
+import { CppMemoryWriter } from './struct-write.js';
+export function allocateRunData(module) {
+ const runDataSize = module._get_rundata_size();
+ return module._calloc_one(runDataSize);
+}
+export function passRunDataToWasm(module, runDataStruct, runData, runDataPointer) {
+ const writer = new CppMemoryWriter(module.HEAP8.buffer, runDataPointer);
+ Object.entries(runData).forEach(([fieldName, value]) => {
+ const fieldLayout = runDataStruct.get(fieldName);
+ if (!fieldLayout) {
+ throw new Error(`Field ${fieldName} not found in RunData struct layout. Please file an issue to satellite.js.`);
+ }
+ writer.writeValue(fieldName, fieldLayout.offset, fieldLayout.type, value, fieldLayout.size);
+ });
+ return runDataPointer;
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/index.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/index.d.ts
new file mode 100644
index 0000000..9465f01
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/index.d.ts
@@ -0,0 +1,5 @@
+import type { MultiThreadRuntime, SingleThreadRuntime } from './wasm-runtime.js';
+export declare function createSingleThreadRuntime(): Promise;
+export declare function createMultiThreadRuntime(options: {
+ threadsCount: number;
+}): Promise;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/index.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/index.js
new file mode 100644
index 0000000..45bce3e
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/index.js
@@ -0,0 +1,10 @@
+import { createMultiThreadRuntimeFromModule } from './multi-thread-runtime.js';
+import { createSingleThreadRuntimeFromModule } from './single-thread-runtime.js';
+export async function createSingleThreadRuntime() {
+ const { default: createWasmModuleSingleThread } = await import('#wasm-single-thread');
+ return createSingleThreadRuntimeFromModule(await createWasmModuleSingleThread());
+}
+export async function createMultiThreadRuntime(options) {
+ const { default: createWasmModuleMultiThread } = await import('#wasm-multi-thread');
+ return createMultiThreadRuntimeFromModule(await createWasmModuleMultiThread(), options);
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/multi-thread-runtime.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/multi-thread-runtime.d.ts
new file mode 100644
index 0000000..849da43
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/multi-thread-runtime.d.ts
@@ -0,0 +1,5 @@
+import type { WasmModuleMultiThread } from './wasm-module-interfaces.js';
+import type { MultiThreadRuntime } from './wasm-runtime.js';
+export declare function createMultiThreadRuntimeFromModule(wasmModule: WasmModuleMultiThread, options: {
+ threadsCount: number;
+}): Promise;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/multi-thread-runtime.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/multi-thread-runtime.js
new file mode 100644
index 0000000..cb77a0e
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/multi-thread-runtime.js
@@ -0,0 +1,34 @@
+import { getNativeStructFieldLayout } from '../native-structs-from-js.js';
+import { passRunDataToWasm } from '../run-data.js';
+export async function createMultiThreadRuntimeFromModule(wasmModule, options) {
+ const runDataStructLayoutStringPointer = wasmModule._create_rundata_struct_layout_string_pointer();
+ const runDataLayout = getNativeStructFieldLayout(runDataStructLayoutStringPointer, wasmModule);
+ wasmModule._free(runDataStructLayoutStringPointer);
+ const originalCompute = wasmModule.cwrap('compute', 'number', ['number', 'number'], { async: true });
+ let isRunning = false;
+ const compute = async (runData, runDataPointer) => {
+ if (isRunning) {
+ throw new Error('Cannot run multiple computations in parallel on the same WASM runtime. Make sure to await for the previous computation to finish before starting a new one.');
+ }
+ isRunning = true;
+ passRunDataToWasm(wasmModule, runDataLayout, runData, runDataPointer);
+ try {
+ await originalCompute(options.threadsCount, runDataPointer);
+ }
+ finally {
+ isRunning = false;
+ }
+ };
+ const runtime = {
+ mode: 'multi',
+ module: wasmModule,
+ compute,
+ dispose: () => {
+ wasmModule._exit_runtime();
+ },
+ [Symbol.dispose]() {
+ this.dispose();
+ },
+ };
+ return runtime;
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/single-thread-runtime.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/single-thread-runtime.d.ts
new file mode 100644
index 0000000..2c33d4e
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/single-thread-runtime.d.ts
@@ -0,0 +1,3 @@
+import type { WasmModuleSingleThread } from './wasm-module-interfaces.js';
+import type { SingleThreadRuntime } from './wasm-runtime.js';
+export declare function createSingleThreadRuntimeFromModule(wasmModule: WasmModuleSingleThread): Promise;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/single-thread-runtime.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/single-thread-runtime.js
new file mode 100644
index 0000000..2758624
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/single-thread-runtime.js
@@ -0,0 +1,20 @@
+import { getNativeStructFieldLayout } from '../native-structs-from-js.js';
+import { passRunDataToWasm } from '../run-data.js';
+export async function createSingleThreadRuntimeFromModule(wasmModule) {
+ const runDataStructLayoutStringPointer = wasmModule._create_rundata_struct_layout_string_pointer();
+ const runDataLayout = getNativeStructFieldLayout(runDataStructLayoutStringPointer, wasmModule);
+ wasmModule._free(runDataStructLayoutStringPointer);
+ const compute = (runData, runDataPointer) => {
+ passRunDataToWasm(wasmModule, runDataLayout, runData, runDataPointer);
+ wasmModule._compute(runDataPointer);
+ };
+ return {
+ mode: 'single',
+ module: wasmModule,
+ compute,
+ dispose: () => {
+ wasmModule._exit_runtime();
+ },
+ [Symbol.dispose]: () => { },
+ };
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-module-interfaces.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-module-interfaces.d.ts
new file mode 100644
index 0000000..6bdd15a
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-module-interfaces.d.ts
@@ -0,0 +1,37 @@
+/**
+ * This interface defines methods and properties which are required from the WebAssembly module.
+ */
+export interface WasmModuleBase {
+ UTF8ToString(ptr: number, maxBytesToRead?: number | undefined, ignoreNul?: boolean | undefined): string;
+ HEAP8: Int8Array;
+ HEAPF64: Float64Array;
+ _get_elsetrec_size(): number;
+ _get_rundata_size(): number;
+ _create_elsetrec_struct_layout_string_pointer(): number;
+ _create_rundata_struct_layout_string_pointer(): number;
+ _free_struct_layout_string(ptr: number): void;
+ _malloc(bytes: number): number;
+ _calloc_one(bytes: number): number;
+ _free(ptr: number): void;
+ _exit_runtime(): void;
+}
+export interface WasmModuleSingleThread extends WasmModuleBase {
+ _compute(runDataPointer: number): void;
+}
+type EmscriptenTypeMap = {
+ number: number;
+ string: string;
+ array: number[];
+ boolean: boolean;
+ void: void;
+};
+type MapArgs = {
+ [K in keyof T]: T[K] extends keyof EmscriptenTypeMap ? EmscriptenTypeMap[T[K]] : never;
+};
+export interface WasmModuleMultiThread extends WasmModuleBase {
+ _compute(threadsCount: number, runDataPointer: number): number;
+ cwrap(ident: string, returnType: Ret, argTypes?: Args, options?: {
+ async: Async;
+ }): (...args: MapArgs) => Async extends true ? Promise : EmscriptenTypeMap[Ret];
+}
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-module-interfaces.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-module-interfaces.js
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-module-interfaces.js
@@ -0,0 +1 @@
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-runtime.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-runtime.d.ts
new file mode 100644
index 0000000..9e2c301
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-runtime.d.ts
@@ -0,0 +1,19 @@
+import type { RunData } from '../run-data.js';
+import type { WasmModuleMultiThread, WasmModuleSingleThread } from './wasm-module-interfaces.js';
+export interface BaseWasmRuntime extends Disposable {
+ mode: string;
+ dispose(): void;
+}
+export interface SingleThreadRuntime extends BaseWasmRuntime {
+ readonly mode: 'single';
+ module: WasmModuleSingleThread;
+ compute(runData: RunData, runDataPointer: number): void;
+}
+export interface MultiThreadRuntime extends BaseWasmRuntime {
+ readonly mode: 'multi';
+ module: WasmModuleMultiThread;
+ compute(runData: RunData, runDataPointer: number): Promise;
+ dispose(): void;
+ [Symbol.dispose](): void;
+}
+export type WasmRuntime = SingleThreadRuntime | MultiThreadRuntime;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-runtime.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-runtime.js
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/runtimes/wasm-runtime.js
@@ -0,0 +1 @@
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-read.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-read.d.ts
new file mode 100644
index 0000000..4e336c5
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-read.d.ts
@@ -0,0 +1,11 @@
+export declare class CppMemoryReader {
+ baseOffset: number;
+ constructor(buffer: ArrayBufferLike, baseOffset?: number);
+ private view;
+ readInt(offset: number): number;
+ readLong(offset: number): number;
+ readString(offset: number, length: number): string;
+ readDouble(offset: number): number;
+ readUnsignedChar(offset: number): number;
+ readChar(offset: number): string;
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-read.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-read.js
new file mode 100644
index 0000000..de62fb5
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-read.js
@@ -0,0 +1,33 @@
+export class CppMemoryReader {
+ baseOffset;
+ constructor(buffer, baseOffset = 0) {
+ this.baseOffset = baseOffset;
+ this.view = new DataView(buffer);
+ }
+ view;
+ readInt(offset) {
+ const value = this.view.getInt32(this.baseOffset + offset, true);
+ return value;
+ }
+ readLong(offset) {
+ // Assuming 'long' is 32-bit in this context
+ return this.readInt(offset);
+ }
+ readString(offset, length) {
+ const bytes = new Uint8Array(this.view.buffer, this.baseOffset + offset, length);
+ const decoder = new TextDecoder();
+ return decoder.decode(bytes);
+ }
+ readDouble(offset) {
+ const value = this.view.getFloat64(this.baseOffset + offset, true);
+ return value;
+ }
+ readUnsignedChar(offset) {
+ const value = this.view.getUint8(this.baseOffset + offset);
+ return value;
+ }
+ readChar(offset) {
+ const value = this.view.getInt8(this.baseOffset + offset);
+ return String.fromCharCode(value);
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-write.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-write.d.ts
new file mode 100644
index 0000000..b32b777
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-write.d.ts
@@ -0,0 +1,13 @@
+import type { NativeFieldType } from './native-field-type.js';
+export declare class CppMemoryWriter {
+ private baseOffset;
+ constructor(buffer: ArrayBufferLike, baseOffset?: number);
+ private view;
+ setBaseOffset(offset: number): void;
+ writeInt(offset: number, value: number): void;
+ writeString(offset: number, value: string, lengthWithNullTerminator: number): void;
+ writeDouble(offset: number, value: number): void;
+ writeChar(offset: number, value: string): void;
+ writeBoolean(offset: number, value: boolean): void;
+ writeValue(fieldName: string, offset: number, type: NativeFieldType, value: unknown, size: number): void;
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-write.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-write.js
new file mode 100644
index 0000000..2cc116f
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/struct-write.js
@@ -0,0 +1,77 @@
+export class CppMemoryWriter {
+ baseOffset;
+ constructor(buffer, baseOffset = 0) {
+ this.baseOffset = baseOffset;
+ this.view = new DataView(buffer);
+ }
+ view;
+ setBaseOffset(offset) {
+ this.baseOffset = offset;
+ }
+ writeInt(offset, value) {
+ this.view.setInt32(this.baseOffset + offset, value, true);
+ }
+ writeString(offset, value, lengthWithNullTerminator) {
+ const encoder = new TextEncoder();
+ const encoded = encoder.encode(value);
+ const bytes = new Uint8Array(this.view.buffer, this.baseOffset + offset, lengthWithNullTerminator);
+ for (let i = 0; i < lengthWithNullTerminator - 1; i++) {
+ // biome-ignore lint/style/noNonNullAssertion: index arithmetic
+ bytes[i] = i < encoded.length ? encoded[i] : 0;
+ }
+ bytes[lengthWithNullTerminator - 1] = 0; // null-terminate the string
+ }
+ writeDouble(offset, value) {
+ this.view.setFloat64(this.baseOffset + offset, value, true);
+ }
+ writeChar(offset, value) {
+ const charCode = value.charCodeAt(0) || 0;
+ this.view.setInt8(this.baseOffset + offset, charCode);
+ }
+ writeBoolean(offset, value) {
+ this.view.setInt8(this.baseOffset + offset, value ? 1 : 0);
+ }
+ writeValue(fieldName, offset, type, value, size) {
+ switch (type) {
+ case 'bool': {
+ // todo test performance without these checks
+ if (typeof value !== 'boolean') {
+ throw new Error(`Expected boolean for ${fieldName}, got ${typeof value}`);
+ }
+ this.writeBoolean(offset, value);
+ break;
+ }
+ case 'double': {
+ if (typeof value !== 'number') {
+ throw new Error(`Expected number for ${fieldName}, got ${typeof value}`);
+ }
+ this.writeDouble(offset, value);
+ break;
+ }
+ case 'int': {
+ if (typeof value !== 'number') {
+ throw new Error(`Expected number for ${fieldName}, got ${typeof value}`);
+ }
+ this.writeInt(offset, value);
+ break;
+ }
+ case 'char': {
+ if (typeof value !== 'string') {
+ throw new Error(`Expected char for ${fieldName}, got "${typeof value}"`);
+ }
+ this.writeChar(offset, value);
+ break;
+ }
+ case 'char[]': {
+ if (typeof value !== 'string') {
+ throw new Error(`Expected string for ${fieldName}, got "${typeof value}"`);
+ }
+ this.writeString(offset, value, size);
+ break;
+ }
+ default: {
+ throw new Error(`Writing type ${type} not implemented (field ${fieldName})`);
+ }
+ }
+ }
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/toposort.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/toposort.d.ts
new file mode 100644
index 0000000..6d38420
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/toposort.d.ts
@@ -0,0 +1,4 @@
+export declare function topologicalSort(items: {
+ provides: T;
+ hasDependencies: readonly T[];
+}[]): T[];
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/toposort.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/toposort.js
new file mode 100644
index 0000000..c434447
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/toposort.js
@@ -0,0 +1,18 @@
+export function topologicalSort(items) {
+ const visited = new Set();
+ let someRemoved = true;
+ while (someRemoved) {
+ someRemoved = false;
+ for (const { provides, hasDependencies } of items) {
+ if (hasDependencies.every((dep) => visited.has(dep)) &&
+ !visited.has(provides)) {
+ visited.add(provides);
+ someRemoved = true;
+ }
+ }
+ }
+ if (visited.size !== items.length) {
+ throw new Error('Cyclic dependency detected');
+ }
+ return Array.from(visited);
+}
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/typed-array.d.ts b/Petabit/wwwroot/lib/satellite-js/dist/wasm/typed-array.d.ts
new file mode 100644
index 0000000..db3d476
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/typed-array.d.ts
@@ -0,0 +1 @@
+export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
diff --git a/Petabit/wwwroot/lib/satellite-js/dist/wasm/typed-array.js b/Petabit/wwwroot/lib/satellite-js/dist/wasm/typed-array.js
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/dist/wasm/typed-array.js
@@ -0,0 +1 @@
+export {};
diff --git a/Petabit/wwwroot/lib/satellite-js/package.json b/Petabit/wwwroot/lib/satellite-js/package.json
new file mode 100644
index 0000000..6c16756
--- /dev/null
+++ b/Petabit/wwwroot/lib/satellite-js/package.json
@@ -0,0 +1 @@
+{"name":"satellite.js","version":"7.1.0","type":"module","license":"MIT"}
diff --git a/README.md b/README.md
index 43e434a..e44f4a2 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Petabit je responzivna i višejezična ASP.NET Core MVC aplikacija za tehnološk
- animirani hologramski prikaz Zemlje, orbite i položaja ISS-a
- LED matrični prikaz lokacije, brzine, posade i spojenih letjelica
- minimalni ISS prikaz kao odvojeno, lagano sučelje nad istim API endpointom
-- kurirani podaci o posadi i letjelicama s poveznicom na NASA-u
+- automatska provjera NASA objava i konfiguracije postaje svake dvije minute, uz pohranu posljednjeg pouzdanog stanja
- responzivan navbar i prikaz prilagođen mobilnim uređajima
- stranice za knjige, aplikacije, blockchain i privatnost
- Google Analytics koji se učitava tek nakon korisničke privole
@@ -47,6 +47,12 @@ Izvori podataka:
- [Where the ISS at? API](https://wheretheiss.at/) — trenutačna lokacija i brzina ISS-a
- [NASA](https://www.nasa.gov/international-space-station/) — referentni podaci o postaji, posadi i letjelicama
+Podaci o posadi i brodovima dolaze iz zasebnog pozadinskog NASA servisa. `/Home/StationData` prikazuje ih neovisno o dostupnosti API-ja za položaj. Početna stranica automatski osvježava prikaz svakih 30 sekundi i razlikuje položaj uživo od potvrđenog stanja postaje. Nejasne promjene zahtijevaju provjeru; najave ne mijenjaju stanje.
+
+Za pohranu između Railway deploymenta potreban je persistent volume na `/app/App_Data`. Detalji konfiguracije, pravila i oporavka: [NASA sinkronizacija](Petabit/docs/nasa-station-sync.md).
+
+`Ping ISS` otvara klizeći LED izvještaj i kompaktni plutajući panel s pet sekundi prijenosa uživo. `Ping Starlink` otvara zaseban globus s kretanjem operativnih satelita, brojem operativnih i ukupnih satelita u orbiti te starošću podataka. Položaji se računaju iz CelesTrak OMM podataka; zajednička predmemorija traje dva sata. Detalji: [Starlink praćenje](Petabit/docs/starlink-tracking.md).
+
## 🛡️ Sigurnost i privatnost
- HTTPS redirekcija i jednogodišnji HSTS u produkciji
@@ -68,7 +74,7 @@ Izvori podataka:
- `/health/live` provjera procesa i `/health/ready` provjera dostupnosti ISS servisa, izuzete iz rate limitinga
- automatski GitHub produkcijski smoke test nakon svakog pusha u `master`, uz ručno pokretanje za ciljanu dijagnostiku
- Better Stack provjera produkcijskog liveness endpointa svakih 10 minuta
-- tjedna provjera starosti kuriranih podataka o ISS posadi i letjelicama
+- provjera produkcijske NASA sinkronizacije svakih sat vremena
- smoke test početne stranice, readiness endpointa i ISS trackera
- strukturirani JSON logovi u produkciji
- validirani `X-Correlation-ID` za povezivanje korisničkog zahtjeva s Railway logovima