@saber-usa/node-common 1.7.17-alpha.1 → 1.7.18-alpha.1
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 +20 -0
- package/package.json +6 -2
- package/src/astro.js +400 -277
- package/src/ballisticPropagator.js +1 -1
- package/src/index.js +3 -0
- package/src/wasmProp/index.js +10 -0
- package/src/wasmProp/primitives.js +295 -0
- package/src/wasmProp/runtime.js +147 -0
- package/src/wasmProp/wasmAstro.js +251 -0
package/src/astro.js
CHANGED
|
@@ -793,6 +793,12 @@ 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
|
+
|
|
796
802
|
const doesLineSegmentSphereIntersect = (linePoint0, linePoint1, circleCenter, circleRadius) => {
|
|
797
803
|
// From Space Cockpit
|
|
798
804
|
// http://www.codeproject.com/Articles/19799/Simple-Ray-Tracing-in-C-Part-II-Triangles-Intersec
|
|
@@ -1725,6 +1731,78 @@ const cartesianToElsetElements = (pv, epoch) => {
|
|
|
1725
1731
|
* @param {Integer} endTime, end time of the analysis, Unix milliseconds
|
|
1726
1732
|
* @return {Array} Array of RPO data objects
|
|
1727
1733
|
*/
|
|
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
|
+
|
|
1728
1806
|
const getLeoRpoData = (line1, line2, sats, startTime, endTime) => {
|
|
1729
1807
|
const results = [];
|
|
1730
1808
|
const pSatRec = checkTle(line1, line2);
|
|
@@ -1732,78 +1810,20 @@ const getLeoRpoData = (line1, line2, sats, startTime, endTime) => {
|
|
|
1732
1810
|
|
|
1733
1811
|
const start = new Date(startTime).getTime();
|
|
1734
1812
|
const end = new Date(endTime).getTime();
|
|
1735
|
-
const pElset = {
|
|
1736
|
-
Line1: line1,
|
|
1737
|
-
Line2: line2,
|
|
1738
|
-
};
|
|
1813
|
+
const pElset = {Line1: line1, Line2: line2};
|
|
1739
1814
|
const pEphem = prop(pElset, start, end, 10000);
|
|
1740
1815
|
if (pEphem.length === 0) {return results;} // Primary may have re-entered the atmosphere
|
|
1741
1816
|
|
|
1742
|
-
sats.forEach(
|
|
1817
|
+
sats.forEach((s) => {
|
|
1743
1818
|
const sEphem = prop(s, start, end, 10000);
|
|
1744
|
-
|
|
1745
|
-
|
|
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);
|
|
1819
|
+
const row = assembleLeoRpoResult(s, pEphem, sEphem);
|
|
1820
|
+
if (isDefined(row)) {results.push(row);}
|
|
1803
1821
|
});
|
|
1804
1822
|
return results;
|
|
1805
1823
|
};
|
|
1806
1824
|
|
|
1825
|
+
// `getLeoRpoDataBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
1826
|
+
|
|
1807
1827
|
|
|
1808
1828
|
/**
|
|
1809
1829
|
* Get GEO RPO data for a given target satellite and a set of potential threat satellites.
|
|
@@ -1815,208 +1835,247 @@ const getLeoRpoData = (line1, line2, sats, startTime, endTime) => {
|
|
|
1815
1835
|
* @param {Integer} lonTime, datetime to analyze longitude and lon drift at, Unix milliseconds
|
|
1816
1836
|
* @return {Array<Objects>}, Array of RPO data objects
|
|
1817
1837
|
*/
|
|
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
|
+
|
|
1818
1952
|
const getGeoRpoData = (line1, line2, sats, startTime, endTime, lonTime) => {
|
|
1953
|
+
const results = [];
|
|
1819
1954
|
const start = new Date(startTime).getTime();
|
|
1820
1955
|
const end = new Date(endTime).getTime();
|
|
1821
|
-
const pEphem = prop({
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
}
|
|
1825
|
-
if (pEphem.length === 0) {return results;} // Primary may have re-entered the atmosphere
|
|
1956
|
+
const pEphem = prop({Line1: line1, Line2: line2}, start, end, 60000);
|
|
1957
|
+
if (!isDefined(pEphem) || pEphem.length === 0) {
|
|
1958
|
+
return results; // Primary may have re-entered the atmosphere
|
|
1959
|
+
}
|
|
1826
1960
|
|
|
1827
1961
|
const lonEvalTime = lonTime ? new Date(lonTime) : new Date(end);
|
|
1828
|
-
|
|
1829
1962
|
const pLonAndDrift = getLonAndDrift(line1, line2, lonEvalTime);
|
|
1830
1963
|
|
|
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 = [];
|
|
1842
1964
|
sats.forEach((s) => {
|
|
1843
|
-
const
|
|
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
|
-
}
|
|
1965
|
+
const sEphem = prop({Line1: s.Line1, Line2: s.Line2}, start, end, 60000);
|
|
1875
1966
|
const sLonAndDrift = getLonAndDrift(s.Line1, s.Line2, lonEvalTime);
|
|
1876
|
-
|
|
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);
|
|
1967
|
+
const row = assembleGeoRpoResult(s, pEphem, sEphem, pLonAndDrift, sLonAndDrift);
|
|
1968
|
+
if (isDefined(row)) {results.push(row);}
|
|
1917
1969
|
});
|
|
1918
1970
|
|
|
1919
1971
|
return results;
|
|
1920
1972
|
};
|
|
1921
1973
|
|
|
1922
|
-
|
|
1923
|
-
// Accuracy is 86400 pooints on a 360 circle (i.e. 0.00416 deg per second)
|
|
1974
|
+
// `getGeoRpoDataBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
1924
1975
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1976
|
+
/** GEO mean-anomaly TLE template used by the shadow-zone scan. The
|
|
1977
|
+
* `XXX.XXXX` token is substituted with each candidate mean anomaly.
|
|
1978
|
+
* TLE lines are fixed-length by definition, hence the disable-next-line. */
|
|
1979
|
+
// eslint-disable-next-line max-len
|
|
1980
|
+
const SHADOW_ZONE_TLE_LINE_1 = "1 00000U 00000A 24079.98445361 -.00000000 00000-0 -00000-0 0 00000";
|
|
1981
|
+
// eslint-disable-next-line max-len
|
|
1982
|
+
const SHADOW_ZONE_TLE_LINE_2_TEMPLATE = "2 00000 000.0000 000.0000 0000000 000.0000 XXX.XXXX 01.00000000000000";
|
|
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);
|
|
1932
1999
|
for (let i = 0; i < steps; i++) {
|
|
1933
|
-
// Calculate the mean anomaly for this step
|
|
1934
2000
|
const meanAnomaly = i * accuracySecondsDeg;
|
|
1935
|
-
// Format the mean anomaly into the TLE line
|
|
1936
|
-
// Round the mean anomaly to two decimal places
|
|
1937
2001
|
const roundedMeanAnomaly = meanAnomaly.toFixed(4);
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
res.push({
|
|
1943
|
-
ecl: getEclipseStatus(time, [p.x, p.y, p.z]),
|
|
1944
|
-
geolon: eciToGeodetic(p, gstime(time)).longitude*RAD2DEG,
|
|
1945
|
-
});
|
|
2002
|
+
const tleLine2 = SHADOW_ZONE_TLE_LINE_2_TEMPLATE.replace(
|
|
2003
|
+
"XXX.XXXX", roundedMeanAnomaly.padStart(8, "0"),
|
|
2004
|
+
);
|
|
2005
|
+
out[i] = {Line1: SHADOW_ZONE_TLE_LINE_1, Line2: tleLine2};
|
|
1946
2006
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
-
});
|
|
2007
|
+
return out;
|
|
2008
|
+
};
|
|
1955
2009
|
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2010
|
+
/**
|
|
2011
|
+
* Pure zone-analysis pass. Takes the per-slot
|
|
2012
|
+
* `[{ecl, geolon}, ...]` series produced by either the JS or WASM scan
|
|
2013
|
+
* and returns `{penStartWestLon, penStartEastLon}` exactly as the
|
|
2014
|
+
* original `getGeoShadowZones` does.
|
|
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}];
|
|
1961
2026
|
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
});
|
|
2027
|
+
for (let i = 1; i <= res.length - 1; i++) {
|
|
2028
|
+
if (res[i].ecl !== res[i - 1].ecl) {
|
|
2029
|
+
ints.at(-1).stopDeg = res[i - 1].geolon;
|
|
2030
|
+
ints.push({ecl: res[i].ecl, startDeg: res[i].geolon});
|
|
1967
2031
|
}
|
|
1968
2032
|
}
|
|
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.
|
|
1972
2033
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
&& ints.filter((x)=>x.ecl === "SUN").length===1) {
|
|
1977
|
-
return {
|
|
1978
|
-
penStartWestLon: null,
|
|
1979
|
-
penStartEastLon: null,
|
|
1980
|
-
};
|
|
2034
|
+
if (ints.length === 1
|
|
2035
|
+
&& ints.filter((x) => x.ecl === "SUN").length === 1) {
|
|
2036
|
+
return {penStartWestLon: null, penStartEastLon: null};
|
|
1981
2037
|
}
|
|
1982
2038
|
|
|
1983
2039
|
ints[0].startDeg = ints.at(-1).startDeg;
|
|
1984
2040
|
ints.pop();
|
|
1985
2041
|
|
|
1986
|
-
if (
|
|
1987
|
-
ints.
|
|
1988
|
-
|
|
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");
|
|
2042
|
+
if (ints.filter((x) => x.ecl === "UMBRA").length === 0
|
|
2043
|
+
&& ints.some((x) => x.ecl === "PENUMBRA")) {
|
|
2044
|
+
const penumbraInt = ints.find((x) => x.ecl === "PENUMBRA");
|
|
1993
2045
|
return {
|
|
1994
2046
|
penStartWestLon: wrapToRange(penumbraInt.startDeg, 0, 360),
|
|
1995
2047
|
penStartEastLon: wrapToRange(penumbraInt.stopDeg, 0, 360),
|
|
1996
2048
|
};
|
|
1997
|
-
}
|
|
1998
|
-
// If sun, umbra, penumbra intervals exist
|
|
1999
|
-
|
|
2000
|
-
const umbra = ints.find((x)=>x.ecl === "UMBRA");
|
|
2049
|
+
}
|
|
2001
2050
|
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2051
|
+
const umbra = ints.find((x) => x.ecl === "UMBRA");
|
|
2052
|
+
const umbraStart360 = wrapToRange(umbra.startDeg, 0, 360);
|
|
2053
|
+
const umbraStop360 = wrapToRange(umbra.stopDeg, 0, 360);
|
|
2054
|
+
const penumbraInts = ints.filter((x) => x.ecl === "PENUMBRA");
|
|
2055
|
+
const chosenPenumbraInt = penumbraInts[0].startDeg > penumbraInts[0].stopDeg
|
|
2056
|
+
? penumbraInts[1] : penumbraInts[0];
|
|
2057
|
+
const penumbraAngle = chosenPenumbraInt.stopDeg - chosenPenumbraInt.startDeg;
|
|
2058
|
+
return {
|
|
2059
|
+
penStartWestLon: umbraStart360 - penumbraAngle,
|
|
2060
|
+
penStartEastLon: umbraStop360 + penumbraAngle,
|
|
2061
|
+
};
|
|
2062
|
+
};
|
|
2012
2063
|
|
|
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;
|
|
2013
2069
|
return {
|
|
2014
|
-
|
|
2015
|
-
|
|
2070
|
+
ecl: getEclipseStatus(time, [p.x, p.y, p.z]),
|
|
2071
|
+
geolon: eciToGeodetic(p, gstime(time)).longitude * RAD2DEG,
|
|
2016
2072
|
};
|
|
2017
|
-
}
|
|
2073
|
+
});
|
|
2074
|
+
return analyzeShadowZoneSeries(res);
|
|
2018
2075
|
};
|
|
2019
2076
|
|
|
2077
|
+
// `getGeoShadowZonesBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
2078
|
+
|
|
2020
2079
|
/** Returns the light intervals of an arbitrary satellite in strict GEO orbit.
|
|
2021
2080
|
*
|
|
2022
2081
|
* These intervals are generally equal in duration for all other GEO satellites.
|
|
@@ -2415,66 +2474,85 @@ const calculateLeoPhaseDifference = (pLine1, pLine2, tLine1, tLine2, time) => {
|
|
|
2415
2474
|
* @param {Integer} stepMs, step time of the analysis in milliseconds, default 10s
|
|
2416
2475
|
* @return {Array} Array of data objects
|
|
2417
2476
|
*/
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2477
|
+
/**
|
|
2478
|
+
* Build the breakpoint schedule for `getLeoWaterfallData` /
|
|
2479
|
+
* `getLeoWaterfallDataBulk`. The waterfall covers `[start, end]` divided
|
|
2480
|
+
* into segments at every per-satellite elset epoch that falls strictly
|
|
2481
|
+
* inside that range. Pure; no propagation.
|
|
2482
|
+
*
|
|
2483
|
+
* Each breakpoint is `{time, satIndex, elsetIndex?}`. The two synthetic
|
|
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) => {
|
|
2425
2493
|
const breakpoints = [{time: start, satIndex: -1}, {time: end, satIndex: -1}];
|
|
2426
2494
|
elsets.forEach((satElsets, i) => {
|
|
2427
2495
|
breakpoints.push(...satElsets.map((e, j) => ({
|
|
2428
2496
|
time: new Date(e.Epoch).getTime(),
|
|
2429
2497
|
satIndex: i,
|
|
2430
2498
|
elsetIndex: j,
|
|
2431
|
-
})).filter((b) => b.time > start && b.time < end));
|
|
2499
|
+
})).filter((b) => b.time > start && b.time < end));
|
|
2432
2500
|
});
|
|
2433
2501
|
breakpoints.sort((a, b) => a.time - b.time);
|
|
2502
|
+
return breakpoints;
|
|
2503
|
+
};
|
|
2434
2504
|
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
});
|
|
2472
|
-
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Decorate a per-segment ephemeris with osculating elements and elset
|
|
2507
|
+
* metadata. Identical post-processing as the original inline `.map()` in
|
|
2508
|
+
* `getLeoWaterfallData`. Used by both the JS and WASM waterfall variants
|
|
2509
|
+
* so the two emit byte-equivalent rows.
|
|
2510
|
+
*
|
|
2511
|
+
* `isEpoch` is `true` for the first point of the segment if-and-only-if
|
|
2512
|
+
* the segment was opened by *this* satellite's elset epoch (matches the
|
|
2513
|
+
* original `pointInd === 0 && bkpoint.satIndex === satIndex` check).
|
|
2514
|
+
*
|
|
2515
|
+
* @param {Array<{p:number[],v:number[],t:number}>} ephem segment ephemeris
|
|
2516
|
+
* @param {Object} elset active elset for this satellite in this segment
|
|
2517
|
+
* @param {{satIndex:number}} bkpoint segment-opening breakpoint
|
|
2518
|
+
* @param {Number} satIndex index of the satellite in the original `elsets`
|
|
2519
|
+
* @return {Array<Object>}
|
|
2520
|
+
*/
|
|
2521
|
+
const decorateWaterfallSegment = (ephem, elset, bkpoint, satIndex) => {
|
|
2522
|
+
const epoch = new Date(elset.Epoch).getTime();
|
|
2523
|
+
return ephem.map((point, pointInd) => {
|
|
2524
|
+
const osculatingElements = cartesianToKeplerian(
|
|
2525
|
+
multiply(posToArray(point.p), 1000), // km -> m
|
|
2526
|
+
multiply(posToArray(point.v), 1000), // km/s -> m/s
|
|
2527
|
+
);
|
|
2528
|
+
return {
|
|
2529
|
+
...point,
|
|
2530
|
+
isEpoch: pointInd === 0 && bkpoint.satIndex === satIndex,
|
|
2531
|
+
epoch: epoch,
|
|
2532
|
+
satNo: elset.SatNo,
|
|
2533
|
+
name: elset.CommonName,
|
|
2534
|
+
source: elset.Source,
|
|
2535
|
+
raan: osculatingElements.raan, // deg
|
|
2536
|
+
raanPrecession: elset.RaanPrecessionDegreesPerDay,
|
|
2537
|
+
inclination: osculatingElements.i, // deg
|
|
2538
|
+
};
|
|
2539
|
+
});
|
|
2540
|
+
};
|
|
2473
2541
|
|
|
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 = [];
|
|
2474
2553
|
const pEphem = satEphems[0];
|
|
2475
2554
|
|
|
2476
|
-
satEphems.forEach(
|
|
2477
|
-
// Find the distance at each time step
|
|
2555
|
+
satEphems.forEach((sEphem, satIndex) => {
|
|
2478
2556
|
for (let i = 0; i < pEphem.length; i++) {
|
|
2479
2557
|
const pEphemPoint = pEphem[i];
|
|
2480
2558
|
const sEphemPoint = sEphem[i];
|
|
@@ -2493,7 +2571,7 @@ const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
|
2493
2571
|
|
|
2494
2572
|
if (satIndex > 0) { // not the primary
|
|
2495
2573
|
if (areCoordsEqual(pEphemPoint.p, sEphemPoint.p)) {
|
|
2496
|
-
catsAngle = 180;
|
|
2574
|
+
catsAngle = 180;
|
|
2497
2575
|
} else {
|
|
2498
2576
|
catsAngle = angleBetween3DCoords(
|
|
2499
2577
|
pEphemPoint.p,
|
|
@@ -2501,7 +2579,6 @@ const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
|
2501
2579
|
getSunDirection(time),
|
|
2502
2580
|
);
|
|
2503
2581
|
}
|
|
2504
|
-
|
|
2505
2582
|
const earthEclipsed = doesLineSegmentIntersectEarth(pEphemPoint.p, sEphemPoint.p);
|
|
2506
2583
|
const targetShadow = getEclipseStatus(time, posToArray(sEphemPoint.p));
|
|
2507
2584
|
targetVisibility = earthEclipsed ? "EARTH ECLIPSED" : targetShadow;
|
|
@@ -2529,6 +2606,40 @@ const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
|
2529
2606
|
return results;
|
|
2530
2607
|
};
|
|
2531
2608
|
|
|
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
|
+
|
|
2532
2643
|
/**
|
|
2533
2644
|
* Returns the minimum deltaV for a Lambert maneuver between two satellites,
|
|
2534
2645
|
* within the given time range.
|
|
@@ -3325,6 +3436,18 @@ export {
|
|
|
3325
3436
|
getGeoRpoData,
|
|
3326
3437
|
getGeoShadowZones,
|
|
3327
3438
|
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,
|
|
3328
3451
|
getEclipseStatus,
|
|
3329
3452
|
estimateSlantRange,
|
|
3330
3453
|
calculateNextApogeePerigeeTimes,
|