@saber-usa/node-common 1.7.18-alpha.1 → 1.7.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -20
- package/package.json +3 -7
- package/src/astro.js +277 -400
- package/src/index.js +0 -3
- package/src/wasmProp/index.js +0 -10
- package/src/wasmProp/primitives.js +0 -295
- package/src/wasmProp/runtime.js +0 -147
- package/src/wasmProp/wasmAstro.js +0 -251
package/src/astro.js
CHANGED
|
@@ -793,12 +793,6 @@ const propGeodetic = (elset, start, end, stepMs = 60000) => {
|
|
|
793
793
|
return positions;
|
|
794
794
|
};
|
|
795
795
|
|
|
796
|
-
// WASM-backed bulk primitives (`propBulk`, `propGeodeticBulk`,
|
|
797
|
-
// `propLookAnglesBulk`) live in `./wasmProp/primitives.js`. The sync primitives
|
|
798
|
-
// above remain the canonical path for single-satellite / small-N callers;
|
|
799
|
-
// the bulk siblings are exposed through the barrel at `./wasmProp/index.js` and
|
|
800
|
-
// re-exported from `./index.js`. See `docs/BULK_PROPAGATION.md`.
|
|
801
|
-
|
|
802
796
|
const doesLineSegmentSphereIntersect = (linePoint0, linePoint1, circleCenter, circleRadius) => {
|
|
803
797
|
// From Space Cockpit
|
|
804
798
|
// http://www.codeproject.com/Articles/19799/Simple-Ray-Tracing-in-C-Part-II-Triangles-Intersec
|
|
@@ -1731,78 +1725,6 @@ const cartesianToElsetElements = (pv, epoch) => {
|
|
|
1731
1725
|
* @param {Integer} endTime, end time of the analysis, Unix milliseconds
|
|
1732
1726
|
* @return {Array} Array of RPO data objects
|
|
1733
1727
|
*/
|
|
1734
|
-
/**
|
|
1735
|
-
* Pure assembly helper for a single LEO RPO comparison row. Takes the primary
|
|
1736
|
-
* and threat ephemerides (already produced by either the JS `prop` path or
|
|
1737
|
-
* the WASM `propBulk` path) and returns the assembled result object. Shared
|
|
1738
|
-
* by `getLeoRpoData` and `getLeoRpoDataBulk` to guarantee the JS and WASM
|
|
1739
|
-
* variants emit byte-identical structures.
|
|
1740
|
-
*
|
|
1741
|
-
* Returns `undefined` if either ephemeris is missing or the lengths do not
|
|
1742
|
-
* agree (which mirrors the original guard, including the case where the
|
|
1743
|
-
* threat re-entered the atmosphere mid-window).
|
|
1744
|
-
*
|
|
1745
|
-
* @param {Object} s threat elset / metadata record
|
|
1746
|
-
* @param {Array<{p:number[],v:number[],t:number}>} pEphem primary ephemeris
|
|
1747
|
-
* @param {Array<{p:number[],v:number[],t:number}>} sEphem threat ephemeris
|
|
1748
|
-
* @return {Object|undefined}
|
|
1749
|
-
*/
|
|
1750
|
-
const assembleLeoRpoResult = (s, pEphem, sEphem) => {
|
|
1751
|
-
if (!isDefined(pEphem)
|
|
1752
|
-
|| !isDefined(sEphem)
|
|
1753
|
-
|| pEphem.length === 0
|
|
1754
|
-
|| pEphem.length !== sEphem.length) {
|
|
1755
|
-
return undefined;
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
const aResult = {
|
|
1759
|
-
line1: s.Line1,
|
|
1760
|
-
line2: s.Line2,
|
|
1761
|
-
epoch: s.Epoch,
|
|
1762
|
-
name: s.CommonName,
|
|
1763
|
-
rank: s.Rank ?? "",
|
|
1764
|
-
satNo: s.SatNo,
|
|
1765
|
-
inclination: s.Inclination,
|
|
1766
|
-
raan: s.Raan,
|
|
1767
|
-
source: s.Source,
|
|
1768
|
-
datamode: s.DataMode,
|
|
1769
|
-
sma: s.SemiMajorAxis,
|
|
1770
|
-
country: s.CountryId,
|
|
1771
|
-
flag: s.Flag,
|
|
1772
|
-
poca: 9999999999,
|
|
1773
|
-
toca: "",
|
|
1774
|
-
tocaString: "",
|
|
1775
|
-
planeDiff: 0,
|
|
1776
|
-
incDiff: s.incDiff,
|
|
1777
|
-
raanDiff: s.raanDiff,
|
|
1778
|
-
semiMajorDiff: s.semiMajorDiff,
|
|
1779
|
-
raanDrift: s.RaanPrecessionDegreesPerDay,
|
|
1780
|
-
};
|
|
1781
|
-
|
|
1782
|
-
const pv1 = {position: pEphem[0].p, velocity: pEphem[0].v};
|
|
1783
|
-
const pv2 = {position: sEphem[0].p, velocity: sEphem[0].v};
|
|
1784
|
-
aResult.di = angleBetweenPlanes(pv1, pv2);
|
|
1785
|
-
const dv = planeChangeDeltaV(pv1, pv2);
|
|
1786
|
-
aResult.dv = {
|
|
1787
|
-
i: Math.round(dv.i * 1000) / 1000,
|
|
1788
|
-
c: Math.round(dv.c * 1000) / 1000,
|
|
1789
|
-
};
|
|
1790
|
-
|
|
1791
|
-
for (let i = 0; i < pEphem.length; i++) {
|
|
1792
|
-
const distKm = dist(pEphem[i].p, sEphem[i].p);
|
|
1793
|
-
if (distKm < aResult.poca) {
|
|
1794
|
-
aResult.poca = distKm;
|
|
1795
|
-
aResult.toca = new Date(pEphem[i].t).toISOString();
|
|
1796
|
-
aResult.tocaString = getTimeDifference(aResult.toca);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
aResult.danger = (aResult.di === 0 || aResult.poca === 0)
|
|
1801
|
-
? 1000
|
|
1802
|
-
: 1 / (aResult.di * aResult.poca);
|
|
1803
|
-
return aResult;
|
|
1804
|
-
};
|
|
1805
|
-
|
|
1806
1728
|
const getLeoRpoData = (line1, line2, sats, startTime, endTime) => {
|
|
1807
1729
|
const results = [];
|
|
1808
1730
|
const pSatRec = checkTle(line1, line2);
|
|
@@ -1810,20 +1732,78 @@ const getLeoRpoData = (line1, line2, sats, startTime, endTime) => {
|
|
|
1810
1732
|
|
|
1811
1733
|
const start = new Date(startTime).getTime();
|
|
1812
1734
|
const end = new Date(endTime).getTime();
|
|
1813
|
-
const pElset = {
|
|
1735
|
+
const pElset = {
|
|
1736
|
+
Line1: line1,
|
|
1737
|
+
Line2: line2,
|
|
1738
|
+
};
|
|
1814
1739
|
const pEphem = prop(pElset, start, end, 10000);
|
|
1815
1740
|
if (pEphem.length === 0) {return results;} // Primary may have re-entered the atmosphere
|
|
1816
1741
|
|
|
1817
|
-
sats.forEach((s) => {
|
|
1742
|
+
sats.forEach( (s) => {
|
|
1818
1743
|
const sEphem = prop(s, start, end, 10000);
|
|
1819
|
-
|
|
1820
|
-
|
|
1744
|
+
if (!isDefined(pEphem)
|
|
1745
|
+
|| !isDefined(sEphem)
|
|
1746
|
+
|| pEphem.length !== sEphem.length) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
const aResult = {
|
|
1751
|
+
line1: s.Line1,
|
|
1752
|
+
line2: s.Line2,
|
|
1753
|
+
epoch: s.Epoch,
|
|
1754
|
+
name: s.CommonName,
|
|
1755
|
+
rank: s.Rank ?? "",
|
|
1756
|
+
satNo: s.SatNo,
|
|
1757
|
+
inclination: s.Inclination,
|
|
1758
|
+
raan: s.Raan,
|
|
1759
|
+
source: s.Source,
|
|
1760
|
+
datamode: s.DataMode,
|
|
1761
|
+
sma: s.SemiMajorAxis,
|
|
1762
|
+
country: s.CountryId,
|
|
1763
|
+
flag: s.Flag,
|
|
1764
|
+
poca: 9999999999,
|
|
1765
|
+
toca: "",
|
|
1766
|
+
tocaString: "",
|
|
1767
|
+
planeDiff: 0,
|
|
1768
|
+
incDiff: s.incDiff,
|
|
1769
|
+
raanDiff: s.raanDiff,
|
|
1770
|
+
semiMajorDiff: s.semiMajorDiff,
|
|
1771
|
+
raanDrift: s.RaanPrecessionDegreesPerDay,
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1774
|
+
const pv1 = {
|
|
1775
|
+
position: pEphem[0].p,
|
|
1776
|
+
velocity: pEphem[0].v,
|
|
1777
|
+
};
|
|
1778
|
+
const pv2 = {
|
|
1779
|
+
position: sEphem[0].p,
|
|
1780
|
+
velocity: sEphem[0].v,
|
|
1781
|
+
};
|
|
1782
|
+
aResult.di = angleBetweenPlanes(pv1, pv2);
|
|
1783
|
+
aResult.dv = planeChangeDeltaV(pv1, pv2);
|
|
1784
|
+
aResult.dv = {
|
|
1785
|
+
i: Math.round(aResult.dv.i*1000)/1000,
|
|
1786
|
+
c: Math.round(aResult.dv.c*1000)/1000,
|
|
1787
|
+
}; // Round to 3 decimals
|
|
1788
|
+
|
|
1789
|
+
// Find the distance at each time step
|
|
1790
|
+
for (let i=0; i<pEphem.length; i++) {
|
|
1791
|
+
const distKm = dist(pEphem[i].p, sEphem[i].p);
|
|
1792
|
+
if (distKm < aResult.poca) {
|
|
1793
|
+
aResult.poca = distKm;
|
|
1794
|
+
aResult.toca = new Date(pEphem[i].t).toISOString();
|
|
1795
|
+
aResult.tocaString = getTimeDifference(aResult.toca);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// Calc danger score (account for di of 0)
|
|
1800
|
+
aResult.danger = (aResult.di === 0
|
|
1801
|
+
|| aResult.poca === 0) ? 1000 : 1/(aResult.di*aResult.poca);
|
|
1802
|
+
results.push(aResult);
|
|
1821
1803
|
});
|
|
1822
1804
|
return results;
|
|
1823
1805
|
};
|
|
1824
1806
|
|
|
1825
|
-
// `getLeoRpoDataBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
1826
|
-
|
|
1827
1807
|
|
|
1828
1808
|
/**
|
|
1829
1809
|
* Get GEO RPO data for a given target satellite and a set of potential threat satellites.
|
|
@@ -1835,247 +1815,208 @@ const getLeoRpoData = (line1, line2, sats, startTime, endTime) => {
|
|
|
1835
1815
|
* @param {Integer} lonTime, datetime to analyze longitude and lon drift at, Unix milliseconds
|
|
1836
1816
|
* @return {Array<Objects>}, Array of RPO data objects
|
|
1837
1817
|
*/
|
|
1838
|
-
/**
|
|
1839
|
-
* GEO-specific signed longitude-difference rate. Encodes whether the two
|
|
1840
|
-
* satellites' longitudes are converging (negative magnitude) or diverging
|
|
1841
|
-
* (positive magnitude) given their respective east-positive drifts. Lifted
|
|
1842
|
-
* verbatim out of `getGeoRpoData`'s nested closure so both the JS and WASM
|
|
1843
|
-
* variants share identical drift accounting.
|
|
1844
|
-
*
|
|
1845
|
-
* @param {Number} lon1 satellite-1 longitude (degrees, signed)
|
|
1846
|
-
* @param {Number} drift1 satellite-1 drift, degrees/day east-positive
|
|
1847
|
-
* @param {Number} lon2 satellite-2 longitude (degrees, signed)
|
|
1848
|
-
* @param {Number} drift2 satellite-2 drift, degrees/day east-positive
|
|
1849
|
-
* @return {Number}
|
|
1850
|
-
*/
|
|
1851
|
-
const geoRelativeLonDrift = (lon1, drift1, lon2, drift2) => {
|
|
1852
|
-
let closing = false;
|
|
1853
|
-
if (lon1 > lon2) {
|
|
1854
|
-
closing = (drift1 * drift2 > 0 && drift1 < drift2)
|
|
1855
|
-
|| (drift1 * drift2 < 0 && drift1 < 0);
|
|
1856
|
-
} else if (lon2 > lon1) {
|
|
1857
|
-
closing = (drift1 * drift2 > 0 && drift2 < drift1)
|
|
1858
|
-
|| (drift1 * drift2 < 0 && drift1 > 0);
|
|
1859
|
-
}
|
|
1860
|
-
const difference = drift1 - drift2;
|
|
1861
|
-
return closing ? -Math.abs(difference) : Math.abs(difference);
|
|
1862
|
-
};
|
|
1863
|
-
|
|
1864
|
-
/**
|
|
1865
|
-
* Pure assembly helper for a single GEO RPO comparison row. Identical math
|
|
1866
|
-
* to the original inline body of `getGeoRpoData`, factored out so the
|
|
1867
|
-
* JS-path (`getGeoRpoData`) and WASM-path (`getGeoRpoDataBulk`) emit
|
|
1868
|
-
* byte-equivalent structures.
|
|
1869
|
-
*
|
|
1870
|
-
* `getLonAndDrift` is JS-only by design (see the `low-value` opportunity
|
|
1871
|
-
* list in `docs/BULK_PROPAGATION.md`); both call sites compute it
|
|
1872
|
-
* upstream and pass the precomputed result in.
|
|
1873
|
-
*
|
|
1874
|
-
* Returns `undefined` if either ephemeris is missing or the lengths do not
|
|
1875
|
-
* match (mirrors the original guard).
|
|
1876
|
-
*
|
|
1877
|
-
* @param {Object} s threat elset / metadata record
|
|
1878
|
-
* @param {Array<{p:number[],v:number[],t:number}>} pEphem primary ephemeris
|
|
1879
|
-
* @param {Array<{p:number[],v:number[],t:number}>} sEphem threat ephemeris
|
|
1880
|
-
* @param {Object} pLonAndDrift output of `getLonAndDrift(primary)`
|
|
1881
|
-
* @param {Object} sLonAndDrift output of `getLonAndDrift(threat)`
|
|
1882
|
-
* @return {Object|undefined}
|
|
1883
|
-
*/
|
|
1884
|
-
const assembleGeoRpoResult = (s, pEphem, sEphem, pLonAndDrift, sLonAndDrift) => {
|
|
1885
|
-
if (!isDefined(pEphem)
|
|
1886
|
-
|| !isDefined(sEphem)
|
|
1887
|
-
|| pEphem.length === 0
|
|
1888
|
-
|| pEphem.length !== sEphem.length) {
|
|
1889
|
-
return undefined;
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
const aResult = {
|
|
1893
|
-
line1: s.Line1,
|
|
1894
|
-
line2: s.Line2,
|
|
1895
|
-
epoch: s.Epoch,
|
|
1896
|
-
name: s.CommonName,
|
|
1897
|
-
rank: s.Rank ?? "",
|
|
1898
|
-
satNo: s.SatNo,
|
|
1899
|
-
source: s.Source,
|
|
1900
|
-
country: s.CountryId,
|
|
1901
|
-
datamode: s.DataMode,
|
|
1902
|
-
flag: s.Flag,
|
|
1903
|
-
poca: 9999999999,
|
|
1904
|
-
toca: "",
|
|
1905
|
-
tocaString: "",
|
|
1906
|
-
incDiff: s.incDiff,
|
|
1907
|
-
longitude: (sLonAndDrift.longitude + 360) % 360,
|
|
1908
|
-
lonDiff: null,
|
|
1909
|
-
relativeDrift: null,
|
|
1910
|
-
di: null,
|
|
1911
|
-
dv: null,
|
|
1912
|
-
danger: null,
|
|
1913
|
-
};
|
|
1914
|
-
|
|
1915
|
-
const lonDiff = Math.abs(
|
|
1916
|
-
(pLonAndDrift.longitude + 360) % 360
|
|
1917
|
-
- (sLonAndDrift.longitude + 360) % 360,
|
|
1918
|
-
);
|
|
1919
|
-
aResult.lonDiff = Math.min(lonDiff, 360 - lonDiff);
|
|
1920
|
-
|
|
1921
|
-
aResult.relativeDrift = geoRelativeLonDrift(
|
|
1922
|
-
pLonAndDrift.longitude,
|
|
1923
|
-
pLonAndDrift.lonDriftDegreesPerDay,
|
|
1924
|
-
sLonAndDrift.longitude,
|
|
1925
|
-
sLonAndDrift.lonDriftDegreesPerDay,
|
|
1926
|
-
);
|
|
1927
|
-
|
|
1928
|
-
const pv1 = {position: pEphem[0].p, velocity: pEphem[0].v};
|
|
1929
|
-
const pv2 = {position: sEphem[0].p, velocity: sEphem[0].v};
|
|
1930
|
-
aResult.di = angleBetweenPlanes(pv1, pv2);
|
|
1931
|
-
const dv = planeChangeDeltaV(pv1, pv2);
|
|
1932
|
-
aResult.dv = {
|
|
1933
|
-
i: Math.round(dv.i * 1000) / 1000,
|
|
1934
|
-
c: Math.round(dv.c * 1000) / 1000,
|
|
1935
|
-
};
|
|
1936
|
-
|
|
1937
|
-
for (let i = 0; i < pEphem.length; i++) {
|
|
1938
|
-
const distKm = dist(pEphem[i].p, sEphem[i].p);
|
|
1939
|
-
if (distKm < aResult.poca) {
|
|
1940
|
-
aResult.poca = distKm;
|
|
1941
|
-
aResult.toca = new Date(pEphem[i].t).toISOString();
|
|
1942
|
-
aResult.tocaString = getTimeDifference(aResult.toca);
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
aResult.danger = (aResult.di === 0 || aResult.poca === 0)
|
|
1947
|
-
? 1000
|
|
1948
|
-
: 1 / (aResult.di * aResult.poca);
|
|
1949
|
-
return aResult;
|
|
1950
|
-
};
|
|
1951
|
-
|
|
1952
1818
|
const getGeoRpoData = (line1, line2, sats, startTime, endTime, lonTime) => {
|
|
1953
|
-
const results = [];
|
|
1954
1819
|
const start = new Date(startTime).getTime();
|
|
1955
1820
|
const end = new Date(endTime).getTime();
|
|
1956
|
-
const pEphem = prop({
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
}
|
|
1821
|
+
const pEphem = prop({
|
|
1822
|
+
Line1: line1,
|
|
1823
|
+
Line2: line2,
|
|
1824
|
+
}, start, end, 60000);
|
|
1825
|
+
if (pEphem.length === 0) {return results;} // Primary may have re-entered the atmosphere
|
|
1960
1826
|
|
|
1961
1827
|
const lonEvalTime = lonTime ? new Date(lonTime) : new Date(end);
|
|
1828
|
+
|
|
1962
1829
|
const pLonAndDrift = getLonAndDrift(line1, line2, lonEvalTime);
|
|
1963
1830
|
|
|
1831
|
+
const getLonDiff = (lon1, drift1, lon2, drift2) => {
|
|
1832
|
+
const closing = lon1 > lon2
|
|
1833
|
+
? (drift1 * drift2 > 0 && drift1 < drift2) || (drift1 * drift2 < 0 && drift1 < 0) // Sat1 is more Eastward
|
|
1834
|
+
: lon2 > lon1
|
|
1835
|
+
? (drift1 * drift2 > 0 && drift2 < drift1) || (drift1 * drift2 < 0 && drift1 > 0) // Sat2 is more Eastward
|
|
1836
|
+
: false;
|
|
1837
|
+
const difference = drift1 - drift2;
|
|
1838
|
+
return closing ? -1 * Math.abs(difference) : Math.abs(difference);
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1841
|
+
const results = [];
|
|
1964
1842
|
sats.forEach((s) => {
|
|
1965
|
-
const
|
|
1843
|
+
const aResult = {
|
|
1844
|
+
line1: s.Line1,
|
|
1845
|
+
line2: s.Line2,
|
|
1846
|
+
epoch: s.Epoch,
|
|
1847
|
+
name: s.CommonName,
|
|
1848
|
+
rank: s.Rank ?? "",
|
|
1849
|
+
satNo: s.SatNo,
|
|
1850
|
+
source: s.Source,
|
|
1851
|
+
country: s.CountryId,
|
|
1852
|
+
datamode: s.DataMode,
|
|
1853
|
+
flag: s.Flag,
|
|
1854
|
+
poca: 9999999999,
|
|
1855
|
+
toca: "",
|
|
1856
|
+
tocaString: "",
|
|
1857
|
+
incDiff: s.incDiff,
|
|
1858
|
+
longitude: null,
|
|
1859
|
+
lonDiff: null,
|
|
1860
|
+
relativeDrift: null,
|
|
1861
|
+
di: null,
|
|
1862
|
+
dv: null,
|
|
1863
|
+
danger: null,
|
|
1864
|
+
};
|
|
1865
|
+
|
|
1866
|
+
const sEphem = prop({
|
|
1867
|
+
Line1: aResult.line1,
|
|
1868
|
+
Line2: aResult.line2,
|
|
1869
|
+
}, start, end, 60000);
|
|
1870
|
+
if (!isDefined(pEphem)
|
|
1871
|
+
|| !isDefined(sEphem)
|
|
1872
|
+
|| pEphem.length !== sEphem.length) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1966
1875
|
const sLonAndDrift = getLonAndDrift(s.Line1, s.Line2, lonEvalTime);
|
|
1967
|
-
|
|
1968
|
-
|
|
1876
|
+
aResult.longitude = (sLonAndDrift.longitude + 360) % 360; // Normalize to 0-360
|
|
1877
|
+
|
|
1878
|
+
const lonDiff = Math.abs(
|
|
1879
|
+
(pLonAndDrift.longitude + 360) % 360
|
|
1880
|
+
- (sLonAndDrift.longitude + 360) % 360,
|
|
1881
|
+
);
|
|
1882
|
+
// To ensure that the smallest "short-way" lon diff is considered
|
|
1883
|
+
aResult.lonDiff = Math.min(lonDiff, 360 - lonDiff);
|
|
1884
|
+
|
|
1885
|
+
aResult.relativeDrift = getLonDiff(
|
|
1886
|
+
pLonAndDrift.longitude,
|
|
1887
|
+
pLonAndDrift.lonDriftDegreesPerDay,
|
|
1888
|
+
sLonAndDrift.longitude,
|
|
1889
|
+
sLonAndDrift.lonDriftDegreesPerDay);
|
|
1890
|
+
const pv1 = {
|
|
1891
|
+
position: pEphem[0].p,
|
|
1892
|
+
velocity: pEphem[0].v,
|
|
1893
|
+
};
|
|
1894
|
+
const pv2 = {
|
|
1895
|
+
position: sEphem[0].p,
|
|
1896
|
+
velocity: sEphem[0].v,
|
|
1897
|
+
};
|
|
1898
|
+
aResult.di = angleBetweenPlanes(pv1, pv2);
|
|
1899
|
+
aResult.dv = {
|
|
1900
|
+
i: Math.round(planeChangeDeltaV(pv1, pv2).i*1000)/1000,
|
|
1901
|
+
c: Math.round(planeChangeDeltaV(pv1, pv2).c*1000)/1000,
|
|
1902
|
+
}; // Round to 3 decimals
|
|
1903
|
+
|
|
1904
|
+
for (let i=0; i<pEphem.length; i++) {
|
|
1905
|
+
const distKm = dist(pEphem[i].p, sEphem[i].p);
|
|
1906
|
+
if (distKm < aResult.poca) {
|
|
1907
|
+
aResult.poca = distKm;
|
|
1908
|
+
aResult.toca = new Date(pEphem[i].t).toISOString();
|
|
1909
|
+
aResult.tocaString = getTimeDifference(aResult.toca);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
// Calc danger score (account for di of 0)
|
|
1913
|
+
aResult.danger = (aResult.di === 0
|
|
1914
|
+
|| aResult.poca === 0) ? 1000 : 1/(aResult.di*aResult.poca);
|
|
1915
|
+
|
|
1916
|
+
results.push(aResult);
|
|
1969
1917
|
});
|
|
1970
1918
|
|
|
1971
1919
|
return results;
|
|
1972
1920
|
};
|
|
1973
1921
|
|
|
1974
|
-
|
|
1922
|
+
const getGeoShadowZones = (time, accuracySecondsDeg=0.00416*100) => {
|
|
1923
|
+
// Accuracy is 86400 pooints on a 360 circle (i.e. 0.00416 deg per second)
|
|
1975
1924
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
//
|
|
1980
|
-
const
|
|
1981
|
-
//
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
/**
|
|
1985
|
-
* Build the array of GEO mean-anomaly elsets the shadow-zone scan
|
|
1986
|
-
* exercises. Pure; no propagation. Produces `360 / accuracySecondsDeg`
|
|
1987
|
-
* elsets, one for each mean anomaly slot, in increasing order.
|
|
1988
|
-
*
|
|
1989
|
-
* @param {Number} accuracySecondsDeg degrees per slot
|
|
1990
|
-
* @return {Array<{Line1:string, Line2:string}>}
|
|
1991
|
-
*/
|
|
1992
|
-
const buildShadowZoneElsets = (accuracySecondsDeg) => {
|
|
1993
|
-
// Use Math.floor so fractional `360 / accuracySecondsDeg` (e.g.
|
|
1994
|
-
// accuracy values that don't divide 360 exactly) still produce a
|
|
1995
|
-
// valid integer step count. Matches the original loop's `i < steps`
|
|
1996
|
-
// behavior, which also truncated.
|
|
1997
|
-
const steps = Math.floor(360 / accuracySecondsDeg);
|
|
1998
|
-
const out = new Array(steps);
|
|
1925
|
+
// Define the TLE parameters of a template TLE in GEO
|
|
1926
|
+
const tleLine1 = "1 00000U 00000A 24079.98445361 -.00000000 00000-0 -00000-0 0 00000";
|
|
1927
|
+
const tleLine2Temp = "2 00000 000.0000 000.0000 0000000 000.0000 XXX.XXXX 01.00000000000000";
|
|
1928
|
+
// Number of steps for the mean anomaly
|
|
1929
|
+
const steps = 360 / accuracySecondsDeg;
|
|
1930
|
+
// Loop over the range of mean anomalies
|
|
1931
|
+
const res = [];
|
|
1999
1932
|
for (let i = 0; i < steps; i++) {
|
|
1933
|
+
// Calculate the mean anomaly for this step
|
|
2000
1934
|
const meanAnomaly = i * accuracySecondsDeg;
|
|
1935
|
+
// Format the mean anomaly into the TLE line
|
|
1936
|
+
// Round the mean anomaly to two decimal places
|
|
2001
1937
|
const roundedMeanAnomaly = meanAnomaly.toFixed(4);
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
1938
|
+
// Format the mean anomaly into the TLE line
|
|
1939
|
+
const tleLine2 = tleLine2Temp.replace("XXX.XXXX", roundedMeanAnomaly.padStart(8, "0"));
|
|
1940
|
+
// Generate the satrec object
|
|
1941
|
+
const p = propagate(twoline2satrec(tleLine1, tleLine2), time).position;
|
|
1942
|
+
res.push({
|
|
1943
|
+
ecl: getEclipseStatus(time, [p.x, p.y, p.z]),
|
|
1944
|
+
geolon: eciToGeodetic(p, gstime(time)).longitude*RAD2DEG,
|
|
1945
|
+
});
|
|
2006
1946
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
1947
|
+
// Find the sun zone, umbra zone, and two penumbra zones.
|
|
1948
|
+
const ints = [];
|
|
2009
1949
|
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
*
|
|
2016
|
-
* Lifted out of the original function body without semantic changes:
|
|
2017
|
-
* - identical interval seam-stitching across the 360°/0° wraparound,
|
|
2018
|
-
* - identical "fully lit GEO belt" early return,
|
|
2019
|
-
* - identical penumbra-only vs. umbra+penumbra branching.
|
|
2020
|
-
*
|
|
2021
|
-
* @param {Array<{ecl:string, geolon:number}>} res
|
|
2022
|
-
* @return {{penStartWestLon:?number, penStartEastLon:?number}}
|
|
2023
|
-
*/
|
|
2024
|
-
const analyzeShadowZoneSeries = (res) => {
|
|
2025
|
-
const ints = [{ecl: res[0].ecl}];
|
|
1950
|
+
// Open the first zone. Note that this may not be the start of this zone but somewhere in the middle.
|
|
1951
|
+
// It's just the start of the mean anomaly seen as a circular buffer.
|
|
1952
|
+
ints.push({
|
|
1953
|
+
ecl: res[0].ecl,
|
|
1954
|
+
});
|
|
2026
1955
|
|
|
2027
|
-
for (let i
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
1956
|
+
for (let i=1; i<=res.length-1; i++) {
|
|
1957
|
+
// Compare with the previous eclipse state
|
|
1958
|
+
if (res[i].ecl!==res[i-1].ecl) {
|
|
1959
|
+
// Close the previous zone
|
|
1960
|
+
ints.at(-1).stopDeg = res[i-1].geolon;
|
|
1961
|
+
|
|
1962
|
+
// Open a new zone
|
|
1963
|
+
ints.push({
|
|
1964
|
+
ecl: res[i].ecl,
|
|
1965
|
+
startDeg: res[i].geolon,
|
|
1966
|
+
});
|
|
2031
1967
|
}
|
|
2032
1968
|
}
|
|
1969
|
+
// With this process, the first interval will have a stop lon but not a start lon, and
|
|
1970
|
+
// the final interval will have a start lon but not a stop lon.
|
|
1971
|
+
// Stitch the first and last intervals together.
|
|
2033
1972
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
1973
|
+
// If not dates are pushed on the first interval, it means it never closed, so it's a fully lit GEO belt.
|
|
1974
|
+
// If there's only sun, return null values
|
|
1975
|
+
if ( ints.length===1
|
|
1976
|
+
&& ints.filter((x)=>x.ecl === "SUN").length===1) {
|
|
1977
|
+
return {
|
|
1978
|
+
penStartWestLon: null,
|
|
1979
|
+
penStartEastLon: null,
|
|
1980
|
+
};
|
|
2037
1981
|
}
|
|
2038
1982
|
|
|
2039
1983
|
ints[0].startDeg = ints.at(-1).startDeg;
|
|
2040
1984
|
ints.pop();
|
|
2041
1985
|
|
|
2042
|
-
if (
|
|
2043
|
-
|
|
2044
|
-
|
|
1986
|
+
if (
|
|
1987
|
+
ints.filter((x)=>x.ecl === "UMBRA").length===0
|
|
1988
|
+
&& ints.some((x)=>x.ecl === "PENUMBRA")) {
|
|
1989
|
+
// If there's no umbra but penumbra exists, return the perumbra values
|
|
1990
|
+
|
|
1991
|
+
// Extract the penumbra interval. In the absence of umbra, a single penumbra interval should exist.
|
|
1992
|
+
const penumbraInt = ints.find((x)=>x.ecl === "PENUMBRA");
|
|
2045
1993
|
return {
|
|
2046
1994
|
penStartWestLon: wrapToRange(penumbraInt.startDeg, 0, 360),
|
|
2047
1995
|
penStartEastLon: wrapToRange(penumbraInt.stopDeg, 0, 360),
|
|
2048
1996
|
};
|
|
2049
|
-
}
|
|
1997
|
+
} else {
|
|
1998
|
+
// If sun, umbra, penumbra intervals exist
|
|
2050
1999
|
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2000
|
+
const umbra = ints.find((x)=>x.ecl === "UMBRA");
|
|
2001
|
+
|
|
2002
|
+
const umbraStart360 = wrapToRange(umbra.startDeg, 0, 360);
|
|
2003
|
+
const umbraStop360 = wrapToRange(umbra.stopDeg, 0, 360);
|
|
2004
|
+
// Assuming a spherical earth for this purpose, the two penumbra intervals should be equal.
|
|
2005
|
+
// Compute the angle of one of the penumbra intervals, the one that does not contain the disconituity.
|
|
2006
|
+
const penumbraInts = ints.filter((x)=>x.ecl === "PENUMBRA");
|
|
2007
|
+
const chosenPenumbraInt = penumbraInts[0].startDeg > penumbraInts[0].stopDeg
|
|
2008
|
+
? penumbraInts[1] : penumbraInts[0];
|
|
2009
|
+
const penumbraAngle = chosenPenumbraInt.stopDeg - chosenPenumbraInt.startDeg;
|
|
2010
|
+
const penumbraStart = umbraStart360 - penumbraAngle;
|
|
2011
|
+
const penumbraStop = umbraStop360 + penumbraAngle;
|
|
2063
2012
|
|
|
2064
|
-
const getGeoShadowZones = (time, accuracySecondsDeg = 0.00416 * 100) => {
|
|
2065
|
-
// Default accuracy: 86400 points on the 360° circle (~0.00416 deg/sec).
|
|
2066
|
-
const elsets = buildShadowZoneElsets(accuracySecondsDeg);
|
|
2067
|
-
const res = elsets.map(({Line1, Line2}) => {
|
|
2068
|
-
const p = propagate(twoline2satrec(Line1, Line2), time).position;
|
|
2069
2013
|
return {
|
|
2070
|
-
|
|
2071
|
-
|
|
2014
|
+
penStartWestLon: penumbraStart,
|
|
2015
|
+
penStartEastLon: penumbraStop,
|
|
2072
2016
|
};
|
|
2073
|
-
}
|
|
2074
|
-
return analyzeShadowZoneSeries(res);
|
|
2017
|
+
}
|
|
2075
2018
|
};
|
|
2076
2019
|
|
|
2077
|
-
// `getGeoShadowZonesBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
2078
|
-
|
|
2079
2020
|
/** Returns the light intervals of an arbitrary satellite in strict GEO orbit.
|
|
2080
2021
|
*
|
|
2081
2022
|
* These intervals are generally equal in duration for all other GEO satellites.
|
|
@@ -2474,85 +2415,66 @@ const calculateLeoPhaseDifference = (pLine1, pLine2, tLine1, tLine2, time) => {
|
|
|
2474
2415
|
* @param {Integer} stepMs, step time of the analysis in milliseconds, default 10s
|
|
2475
2416
|
* @return {Array} Array of data objects
|
|
2476
2417
|
*/
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
* sentinel breakpoints at `start` / `end` carry `satIndex: -1` so the
|
|
2485
|
-
* segment loop can distinguish "edge of window" from "satellite epoch".
|
|
2486
|
-
*
|
|
2487
|
-
* @param {Array<Array<Object>>} elsets per-satellite elset arrays
|
|
2488
|
-
* @param {Number} start Unix ms
|
|
2489
|
-
* @param {Number} end Unix ms
|
|
2490
|
-
* @return {Array<{time:number, satIndex:number, elsetIndex?:number}>}
|
|
2491
|
-
*/
|
|
2492
|
-
const buildWaterfallBreakpoints = (elsets, start, end) => {
|
|
2418
|
+
const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
2419
|
+
const results = [];
|
|
2420
|
+
|
|
2421
|
+
const start = new Date(startTime).getTime();
|
|
2422
|
+
const end = new Date(endTime).getTime();
|
|
2423
|
+
|
|
2424
|
+
const currentIndices = elsets.map((sat) => 0);
|
|
2493
2425
|
const breakpoints = [{time: start, satIndex: -1}, {time: end, satIndex: -1}];
|
|
2494
2426
|
elsets.forEach((satElsets, i) => {
|
|
2495
2427
|
breakpoints.push(...satElsets.map((e, j) => ({
|
|
2496
2428
|
time: new Date(e.Epoch).getTime(),
|
|
2497
2429
|
satIndex: i,
|
|
2498
2430
|
elsetIndex: j,
|
|
2499
|
-
})).filter((b) => b.time > start && b.time < end));
|
|
2431
|
+
})).filter((b) => b.time > start && b.time < end)); // ensure breakpoints are inside time range
|
|
2500
2432
|
});
|
|
2501
2433
|
breakpoints.sort((a, b) => a.time - b.time);
|
|
2502
|
-
return breakpoints;
|
|
2503
|
-
};
|
|
2504
2434
|
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
*
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
};
|
|
2435
|
+
const satEphems = elsets.map((sat) => []);
|
|
2436
|
+
|
|
2437
|
+
for (let i = 0; i < breakpoints.length-1; i++) {
|
|
2438
|
+
const bkpoint = breakpoints[i];
|
|
2439
|
+
// if we start at breakpoints, it's irregular (since the epochs aren't in lockstep)
|
|
2440
|
+
// we can force it to Math.ceil(segmentStart/stepMs)*stepMs for even steps but since the discrepancy
|
|
2441
|
+
// is less than 10s in a plot over multiple days it's rather negligible
|
|
2442
|
+
// also this makes it easier since we can check if its an epoch based on the segmentStart
|
|
2443
|
+
const segmentStart = bkpoint.time;
|
|
2444
|
+
const segmentEnd = breakpoints[i+1].time;
|
|
2445
|
+
|
|
2446
|
+
if (bkpoint.satIndex >= 0) { // this is a satellite's elset's epoch
|
|
2447
|
+
currentIndices[bkpoint.satIndex] = bkpoint.elsetIndex;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
currentIndices.forEach((elsetIndex, satIndex) => {
|
|
2451
|
+
const elset = elsets[satIndex][elsetIndex];
|
|
2452
|
+
const epoch = new Date(elset.Epoch).getTime();
|
|
2453
|
+
const ephem = prop(elset, segmentStart, segmentEnd, stepMs);
|
|
2454
|
+
satEphems[satIndex].push(...ephem.map((point, pointInd) => {
|
|
2455
|
+
const osculatingElements = cartesianToKeplerian(
|
|
2456
|
+
multiply(posToArray(point.p), 1000), // km to m
|
|
2457
|
+
multiply(posToArray(point.v), 1000), // km/s to m/s
|
|
2458
|
+
);
|
|
2459
|
+
return {
|
|
2460
|
+
...point,
|
|
2461
|
+
isEpoch: pointInd === 0 && bkpoint.satIndex === satIndex,
|
|
2462
|
+
epoch: epoch,
|
|
2463
|
+
satNo: elset.SatNo,
|
|
2464
|
+
name: elset.CommonName,
|
|
2465
|
+
source: elset.Source,
|
|
2466
|
+
raan: osculatingElements.raan, // deg
|
|
2467
|
+
raanPrecession: elset.RaanPrecessionDegreesPerDay,
|
|
2468
|
+
inclination: osculatingElements.i, // deg
|
|
2469
|
+
};
|
|
2470
|
+
}));
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2541
2473
|
|
|
2542
|
-
/**
|
|
2543
|
-
* Build the per-(time, satellite) waterfall result rows from the
|
|
2544
|
-
* decorated ephemerides produced by {@link decorateWaterfallSegment}.
|
|
2545
|
-
* Identical math to the second loop of the original `getLeoWaterfallData`.
|
|
2546
|
-
* Pure; deterministic; shared by both variants.
|
|
2547
|
-
*
|
|
2548
|
-
* @param {Array<Array<Object>>} satEphems primary at index 0
|
|
2549
|
-
* @return {Array<Object>}
|
|
2550
|
-
*/
|
|
2551
|
-
const assembleWaterfallRows = (satEphems) => {
|
|
2552
|
-
const results = [];
|
|
2553
2474
|
const pEphem = satEphems[0];
|
|
2554
2475
|
|
|
2555
|
-
satEphems.forEach((sEphem, satIndex) => {
|
|
2476
|
+
satEphems.forEach( (sEphem, satIndex) => {
|
|
2477
|
+
// Find the distance at each time step
|
|
2556
2478
|
for (let i = 0; i < pEphem.length; i++) {
|
|
2557
2479
|
const pEphemPoint = pEphem[i];
|
|
2558
2480
|
const sEphemPoint = sEphem[i];
|
|
@@ -2571,7 +2493,7 @@ const assembleWaterfallRows = (satEphems) => {
|
|
|
2571
2493
|
|
|
2572
2494
|
if (satIndex > 0) { // not the primary
|
|
2573
2495
|
if (areCoordsEqual(pEphemPoint.p, sEphemPoint.p)) {
|
|
2574
|
-
catsAngle = 180;
|
|
2496
|
+
catsAngle = 180; // they're in the same spot, so just say secondary can see primary
|
|
2575
2497
|
} else {
|
|
2576
2498
|
catsAngle = angleBetween3DCoords(
|
|
2577
2499
|
pEphemPoint.p,
|
|
@@ -2579,6 +2501,7 @@ const assembleWaterfallRows = (satEphems) => {
|
|
|
2579
2501
|
getSunDirection(time),
|
|
2580
2502
|
);
|
|
2581
2503
|
}
|
|
2504
|
+
|
|
2582
2505
|
const earthEclipsed = doesLineSegmentIntersectEarth(pEphemPoint.p, sEphemPoint.p);
|
|
2583
2506
|
const targetShadow = getEclipseStatus(time, posToArray(sEphemPoint.p));
|
|
2584
2507
|
targetVisibility = earthEclipsed ? "EARTH ECLIPSED" : targetShadow;
|
|
@@ -2606,40 +2529,6 @@ const assembleWaterfallRows = (satEphems) => {
|
|
|
2606
2529
|
return results;
|
|
2607
2530
|
};
|
|
2608
2531
|
|
|
2609
|
-
const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
2610
|
-
const start = new Date(startTime).getTime();
|
|
2611
|
-
const end = new Date(endTime).getTime();
|
|
2612
|
-
|
|
2613
|
-
const breakpoints = buildWaterfallBreakpoints(elsets, start, end);
|
|
2614
|
-
const currentIndices = elsets.map(() => 0);
|
|
2615
|
-
const satEphems = elsets.map(() => []);
|
|
2616
|
-
|
|
2617
|
-
for (let i = 0; i < breakpoints.length - 1; i++) {
|
|
2618
|
-
const bkpoint = breakpoints[i];
|
|
2619
|
-
// Segment boundaries align with elset epochs, not multiples of `stepMs`.
|
|
2620
|
-
// The intra-segment ephemeris drift (<= one step) is negligible at the
|
|
2621
|
-
// multi-day plot resolution this function targets.
|
|
2622
|
-
const segmentStart = bkpoint.time;
|
|
2623
|
-
const segmentEnd = breakpoints[i + 1].time;
|
|
2624
|
-
|
|
2625
|
-
if (bkpoint.satIndex >= 0) {
|
|
2626
|
-
currentIndices[bkpoint.satIndex] = bkpoint.elsetIndex;
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
currentIndices.forEach((elsetIndex, satIndex) => {
|
|
2630
|
-
const elset = elsets[satIndex][elsetIndex];
|
|
2631
|
-
const ephem = prop(elset, segmentStart, segmentEnd, stepMs);
|
|
2632
|
-
satEphems[satIndex].push(
|
|
2633
|
-
...decorateWaterfallSegment(ephem, elset, bkpoint, satIndex),
|
|
2634
|
-
);
|
|
2635
|
-
});
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
return assembleWaterfallRows(satEphems);
|
|
2639
|
-
};
|
|
2640
|
-
|
|
2641
|
-
// `getLeoWaterfallDataBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
2642
|
-
|
|
2643
2532
|
/**
|
|
2644
2533
|
* Returns the minimum deltaV for a Lambert maneuver between two satellites,
|
|
2645
2534
|
* within the given time range.
|
|
@@ -3436,18 +3325,6 @@ export {
|
|
|
3436
3325
|
getGeoRpoData,
|
|
3437
3326
|
getGeoShadowZones,
|
|
3438
3327
|
getGeoLightIntervals,
|
|
3439
|
-
// Shared pure assembly helpers, exported so `./wasmProp/wasmAstro.js` can
|
|
3440
|
-
// consume them without duplicating logic. These are cross-path
|
|
3441
|
-
// building blocks (used by both the JS-sync and WASM-async siblings)
|
|
3442
|
-
// and are safe to call as ordinary utilities.
|
|
3443
|
-
assembleLeoRpoResult,
|
|
3444
|
-
geoRelativeLonDrift,
|
|
3445
|
-
assembleGeoRpoResult,
|
|
3446
|
-
buildShadowZoneElsets,
|
|
3447
|
-
analyzeShadowZoneSeries,
|
|
3448
|
-
buildWaterfallBreakpoints,
|
|
3449
|
-
decorateWaterfallSegment,
|
|
3450
|
-
assembleWaterfallRows,
|
|
3451
3328
|
getEclipseStatus,
|
|
3452
3329
|
estimateSlantRange,
|
|
3453
3330
|
calculateNextApogeePerigeeTimes,
|