@saber-usa/node-common 1.7.18 → 1.7.20
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 +458 -284
- 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
|
|
@@ -848,6 +854,17 @@ const doesLineSegmentIntersectEarth = (start, end) => {
|
|
|
848
854
|
{x: 0, y: 0, z: 0}, WGS72_EARTH_EQUATORIAL_RADIUS_KM);
|
|
849
855
|
};
|
|
850
856
|
|
|
857
|
+
const getRIC = (prim, target) => {
|
|
858
|
+
const deltaR = {
|
|
859
|
+
x: prim.p.x - target.p.x,
|
|
860
|
+
y: prim.p.y - target.p.y,
|
|
861
|
+
z: prim.p.z - target.p.z,
|
|
862
|
+
};
|
|
863
|
+
const cartToRICMatrix = cartesianToRIC(prim.p, prim.v);
|
|
864
|
+
const ricP = multiplyVector(deltaR, cartToRICMatrix);
|
|
865
|
+
return ricP;
|
|
866
|
+
};
|
|
867
|
+
|
|
851
868
|
/**
|
|
852
869
|
* Given primary and target ephems, calculate the time-based, radial, intrack, and crosstrack data
|
|
853
870
|
*
|
|
@@ -881,13 +898,7 @@ const getTRIC = (pEphem, tEphem) => {
|
|
|
881
898
|
datamodes.push(prim.datamode + ", " + target.datamode);
|
|
882
899
|
|
|
883
900
|
// Calculate RIC values.
|
|
884
|
-
const
|
|
885
|
-
x: prim.p.x - target.p.x,
|
|
886
|
-
y: prim.p.y - target.p.y,
|
|
887
|
-
z: prim.p.z - target.p.z,
|
|
888
|
-
};
|
|
889
|
-
const cartToRICMatrix = cartesianToRIC(prim.p, prim.v);
|
|
890
|
-
const ricP = multiplyVector(deltaR, cartToRICMatrix);
|
|
901
|
+
const ricP = getRIC(prim, target);
|
|
891
902
|
r.push(-1 * Math.round(ricP.x * 10000) / 10000);
|
|
892
903
|
i.push(-1 * Math.round(ricP.y * 10000) / 10000);
|
|
893
904
|
c.push(-1 * Math.round(ricP.z * 10000) / 10000);
|
|
@@ -1725,85 +1736,122 @@ const cartesianToElsetElements = (pv, epoch) => {
|
|
|
1725
1736
|
* @param {Integer} endTime, end time of the analysis, Unix milliseconds
|
|
1726
1737
|
* @return {Array} Array of RPO data objects
|
|
1727
1738
|
*/
|
|
1728
|
-
|
|
1739
|
+
/**
|
|
1740
|
+
* Pure assembly helper for a single LEO RPO comparison row. Takes the primary
|
|
1741
|
+
* and threat ephemerides (already produced by either the JS `prop` path or
|
|
1742
|
+
* the WASM `propBulk` path) and returns the assembled result object. Shared
|
|
1743
|
+
* by `getLeoRpoData` and `getLeoRpoDataBulk` to guarantee the JS and WASM
|
|
1744
|
+
* variants emit byte-identical structures.
|
|
1745
|
+
*
|
|
1746
|
+
* Returns `undefined` if either ephemeris is missing or the lengths do not
|
|
1747
|
+
* agree (which mirrors the original guard, including the case where the
|
|
1748
|
+
* threat re-entered the atmosphere mid-window).
|
|
1749
|
+
*
|
|
1750
|
+
* @param {Object} s threat elset / metadata record
|
|
1751
|
+
* @param {Array<{p:number[],v:number[],t:number}>} pEphem primary ephemeris
|
|
1752
|
+
* @param {Array<{p:number[],v:number[],t:number}>} sEphem threat ephemeris
|
|
1753
|
+
* @return {Object|undefined}
|
|
1754
|
+
*/
|
|
1755
|
+
const assembleLeoRpoResult = (s, pEphem, sEphem, includeRIC=false) => {
|
|
1756
|
+
if (!isDefined(pEphem)
|
|
1757
|
+
|| !isDefined(sEphem)
|
|
1758
|
+
|| pEphem.length === 0
|
|
1759
|
+
|| pEphem.length !== sEphem.length) {
|
|
1760
|
+
return undefined;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
const aResult = {
|
|
1764
|
+
line1: s.Line1,
|
|
1765
|
+
line2: s.Line2,
|
|
1766
|
+
epoch: s.Epoch,
|
|
1767
|
+
name: s.CommonName,
|
|
1768
|
+
rank: s.Rank ?? "",
|
|
1769
|
+
satNo: s.SatNo,
|
|
1770
|
+
inclination: s.Inclination,
|
|
1771
|
+
raan: s.Raan,
|
|
1772
|
+
source: s.Source,
|
|
1773
|
+
datamode: s.DataMode,
|
|
1774
|
+
sma: s.SemiMajorAxis,
|
|
1775
|
+
country: s.CountryId,
|
|
1776
|
+
flag: s.Flag,
|
|
1777
|
+
poca: 9999999999,
|
|
1778
|
+
toca: "",
|
|
1779
|
+
tocaString: "",
|
|
1780
|
+
planeDiff: 0,
|
|
1781
|
+
incDiff: s.incDiff,
|
|
1782
|
+
raanDiff: s.raanDiff,
|
|
1783
|
+
semiMajorDiff: s.semiMajorDiff,
|
|
1784
|
+
raanDrift: s.RaanPrecessionDegreesPerDay,
|
|
1785
|
+
...(includeRIC ? {
|
|
1786
|
+
r: [],
|
|
1787
|
+
i: [],
|
|
1788
|
+
c: [],
|
|
1789
|
+
d: [],
|
|
1790
|
+
t: [],
|
|
1791
|
+
} : {})
|
|
1792
|
+
};
|
|
1793
|
+
|
|
1794
|
+
const pv1 = {position: pEphem[0].p, velocity: pEphem[0].v};
|
|
1795
|
+
const pv2 = {position: sEphem[0].p, velocity: sEphem[0].v};
|
|
1796
|
+
aResult.di = angleBetweenPlanes(pv1, pv2);
|
|
1797
|
+
const dv = planeChangeDeltaV(pv1, pv2);
|
|
1798
|
+
aResult.dv = {
|
|
1799
|
+
i: Math.round(dv.i * 1000) / 1000,
|
|
1800
|
+
c: Math.round(dv.c * 1000) / 1000,
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
for (let i = 0; i < pEphem.length; i++) {
|
|
1804
|
+
const prim = pEphem[i];
|
|
1805
|
+
const target = sEphem[i];
|
|
1806
|
+
|
|
1807
|
+
const distKm = dist(prim.p, target.p);
|
|
1808
|
+
|
|
1809
|
+
if(includeRIC) {
|
|
1810
|
+
const ricP = getRIC(prim, target);
|
|
1811
|
+
|
|
1812
|
+
aResult.r.push(-1 * ricP.x);
|
|
1813
|
+
aResult.i.push(-1 * ricP.y);
|
|
1814
|
+
aResult.c.push(-1 * ricP.z);
|
|
1815
|
+
|
|
1816
|
+
aResult.t.push(prim.t);
|
|
1817
|
+
|
|
1818
|
+
aResult.d.push(distKm);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
if (distKm < aResult.poca) {
|
|
1822
|
+
aResult.poca = distKm;
|
|
1823
|
+
aResult.toca = new Date(prim.t).toISOString();
|
|
1824
|
+
aResult.tocaString = getTimeDifference(aResult.toca);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
aResult.danger = (aResult.di === 0 || aResult.poca === 0)
|
|
1829
|
+
? 1000
|
|
1830
|
+
: 1 / (aResult.di * aResult.poca);
|
|
1831
|
+
return aResult;
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1834
|
+
const getLeoRpoData = (line1, line2, sats, startTime, endTime, includeRIC=false) => {
|
|
1729
1835
|
const results = [];
|
|
1730
1836
|
const pSatRec = checkTle(line1, line2);
|
|
1731
1837
|
if (!isDefined(pSatRec)) {return results;}
|
|
1732
1838
|
|
|
1733
1839
|
const start = new Date(startTime).getTime();
|
|
1734
1840
|
const end = new Date(endTime).getTime();
|
|
1735
|
-
const pElset = {
|
|
1736
|
-
Line1: line1,
|
|
1737
|
-
Line2: line2,
|
|
1738
|
-
};
|
|
1841
|
+
const pElset = {Line1: line1, Line2: line2};
|
|
1739
1842
|
const pEphem = prop(pElset, start, end, 10000);
|
|
1740
1843
|
if (pEphem.length === 0) {return results;} // Primary may have re-entered the atmosphere
|
|
1741
1844
|
|
|
1742
|
-
sats.forEach(
|
|
1845
|
+
sats.forEach((s) => {
|
|
1743
1846
|
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);
|
|
1847
|
+
const row = assembleLeoRpoResult(s, pEphem, sEphem, includeRIC);
|
|
1848
|
+
if (isDefined(row)) {results.push(row);}
|
|
1803
1849
|
});
|
|
1804
1850
|
return results;
|
|
1805
1851
|
};
|
|
1806
1852
|
|
|
1853
|
+
// `getLeoRpoDataBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
1854
|
+
|
|
1807
1855
|
|
|
1808
1856
|
/**
|
|
1809
1857
|
* Get GEO RPO data for a given target satellite and a set of potential threat satellites.
|
|
@@ -1815,208 +1863,270 @@ const getLeoRpoData = (line1, line2, sats, startTime, endTime) => {
|
|
|
1815
1863
|
* @param {Integer} lonTime, datetime to analyze longitude and lon drift at, Unix milliseconds
|
|
1816
1864
|
* @return {Array<Objects>}, Array of RPO data objects
|
|
1817
1865
|
*/
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1866
|
+
/**
|
|
1867
|
+
* GEO-specific signed longitude-difference rate. Encodes whether the two
|
|
1868
|
+
* satellites' longitudes are converging (negative magnitude) or diverging
|
|
1869
|
+
* (positive magnitude) given their respective east-positive drifts. Lifted
|
|
1870
|
+
* verbatim out of `getGeoRpoData`'s nested closure so both the JS and WASM
|
|
1871
|
+
* variants share identical drift accounting.
|
|
1872
|
+
*
|
|
1873
|
+
* @param {Number} lon1 satellite-1 longitude (degrees, signed)
|
|
1874
|
+
* @param {Number} drift1 satellite-1 drift, degrees/day east-positive
|
|
1875
|
+
* @param {Number} lon2 satellite-2 longitude (degrees, signed)
|
|
1876
|
+
* @param {Number} drift2 satellite-2 drift, degrees/day east-positive
|
|
1877
|
+
* @return {Number}
|
|
1878
|
+
*/
|
|
1879
|
+
const geoRelativeLonDrift = (lon1, drift1, lon2, drift2) => {
|
|
1880
|
+
let closing = false;
|
|
1881
|
+
if (lon1 > lon2) {
|
|
1882
|
+
closing = (drift1 * drift2 > 0 && drift1 < drift2)
|
|
1883
|
+
|| (drift1 * drift2 < 0 && drift1 < 0);
|
|
1884
|
+
} else if (lon2 > lon1) {
|
|
1885
|
+
closing = (drift1 * drift2 > 0 && drift2 < drift1)
|
|
1886
|
+
|| (drift1 * drift2 < 0 && drift1 > 0);
|
|
1887
|
+
}
|
|
1888
|
+
const difference = drift1 - drift2;
|
|
1889
|
+
return closing ? -Math.abs(difference) : Math.abs(difference);
|
|
1890
|
+
};
|
|
1826
1891
|
|
|
1827
|
-
|
|
1892
|
+
/**
|
|
1893
|
+
* Pure assembly helper for a single GEO RPO comparison row. Identical math
|
|
1894
|
+
* to the original inline body of `getGeoRpoData`, factored out so the
|
|
1895
|
+
* JS-path (`getGeoRpoData`) and WASM-path (`getGeoRpoDataBulk`) emit
|
|
1896
|
+
* byte-equivalent structures.
|
|
1897
|
+
*
|
|
1898
|
+
* `getLonAndDrift` is JS-only by design (see the `low-value` opportunity
|
|
1899
|
+
* list in `docs/BULK_PROPAGATION.md`); both call sites compute it
|
|
1900
|
+
* upstream and pass the precomputed result in.
|
|
1901
|
+
*
|
|
1902
|
+
* Returns `undefined` if either ephemeris is missing or the lengths do not
|
|
1903
|
+
* match (mirrors the original guard).
|
|
1904
|
+
*
|
|
1905
|
+
* @param {Object} s threat elset / metadata record
|
|
1906
|
+
* @param {Array<{p:number[],v:number[],t:number}>} pEphem primary ephemeris
|
|
1907
|
+
* @param {Array<{p:number[],v:number[],t:number}>} sEphem threat ephemeris
|
|
1908
|
+
* @param {Object} pLonAndDrift output of `getLonAndDrift(primary)`
|
|
1909
|
+
* @param {Object} sLonAndDrift output of `getLonAndDrift(threat)`
|
|
1910
|
+
* @return {Object|undefined}
|
|
1911
|
+
*/
|
|
1912
|
+
const assembleGeoRpoResult = (s, pEphem, sEphem, pLonAndDrift, sLonAndDrift, includeRIC=false) => {
|
|
1913
|
+
if (!isDefined(pEphem)
|
|
1914
|
+
|| !isDefined(sEphem)
|
|
1915
|
+
|| pEphem.length === 0
|
|
1916
|
+
|| pEphem.length !== sEphem.length) {
|
|
1917
|
+
return undefined;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const aResult = {
|
|
1921
|
+
line1: s.Line1,
|
|
1922
|
+
line2: s.Line2,
|
|
1923
|
+
epoch: s.Epoch,
|
|
1924
|
+
name: s.CommonName,
|
|
1925
|
+
rank: s.Rank ?? "",
|
|
1926
|
+
satNo: s.SatNo,
|
|
1927
|
+
source: s.Source,
|
|
1928
|
+
country: s.CountryId,
|
|
1929
|
+
datamode: s.DataMode,
|
|
1930
|
+
flag: s.Flag,
|
|
1931
|
+
poca: 9999999999,
|
|
1932
|
+
toca: "",
|
|
1933
|
+
tocaString: "",
|
|
1934
|
+
incDiff: s.incDiff,
|
|
1935
|
+
longitude: (sLonAndDrift.longitude + 360) % 360,
|
|
1936
|
+
lonDiff: null,
|
|
1937
|
+
relativeDrift: null,
|
|
1938
|
+
di: null,
|
|
1939
|
+
dv: null,
|
|
1940
|
+
danger: null,
|
|
1941
|
+
...(includeRIC ? {
|
|
1942
|
+
r: [],
|
|
1943
|
+
i: [],
|
|
1944
|
+
c: [],
|
|
1945
|
+
d: [],
|
|
1946
|
+
t: [],
|
|
1947
|
+
} : {})
|
|
1948
|
+
};
|
|
1828
1949
|
|
|
1829
|
-
const
|
|
1950
|
+
const lonDiff = Math.abs(
|
|
1951
|
+
(pLonAndDrift.longitude + 360) % 360
|
|
1952
|
+
- (sLonAndDrift.longitude + 360) % 360,
|
|
1953
|
+
);
|
|
1954
|
+
aResult.lonDiff = Math.min(lonDiff, 360 - lonDiff);
|
|
1955
|
+
|
|
1956
|
+
aResult.relativeDrift = geoRelativeLonDrift(
|
|
1957
|
+
pLonAndDrift.longitude,
|
|
1958
|
+
pLonAndDrift.lonDriftDegreesPerDay,
|
|
1959
|
+
sLonAndDrift.longitude,
|
|
1960
|
+
sLonAndDrift.lonDriftDegreesPerDay,
|
|
1961
|
+
);
|
|
1830
1962
|
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
return closing ? -1 * Math.abs(difference) : Math.abs(difference);
|
|
1963
|
+
const pv1 = {position: pEphem[0].p, velocity: pEphem[0].v};
|
|
1964
|
+
const pv2 = {position: sEphem[0].p, velocity: sEphem[0].v};
|
|
1965
|
+
aResult.di = angleBetweenPlanes(pv1, pv2);
|
|
1966
|
+
const dv = planeChangeDeltaV(pv1, pv2);
|
|
1967
|
+
aResult.dv = {
|
|
1968
|
+
i: Math.round(dv.i * 1000) / 1000,
|
|
1969
|
+
c: Math.round(dv.c * 1000) / 1000,
|
|
1839
1970
|
};
|
|
1840
1971
|
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
const
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
-
};
|
|
1972
|
+
for (let i = 0; i < pEphem.length; i++) {
|
|
1973
|
+
const prim = pEphem[i];
|
|
1974
|
+
const target = sEphem[i];
|
|
1975
|
+
|
|
1976
|
+
const distKm = dist(prim.p, target.p);
|
|
1977
|
+
|
|
1978
|
+
if(includeRIC) {
|
|
1979
|
+
const ricP = getRIC(prim, target);
|
|
1865
1980
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
return;
|
|
1981
|
+
aResult.r.push(-1 * ricP.x);
|
|
1982
|
+
aResult.i.push(-1 * ricP.y);
|
|
1983
|
+
aResult.c.push(-1 * ricP.z);
|
|
1984
|
+
|
|
1985
|
+
aResult.t.push(prim.t);
|
|
1986
|
+
|
|
1987
|
+
aResult.d.push(distKm);
|
|
1874
1988
|
}
|
|
1875
|
-
const sLonAndDrift = getLonAndDrift(s.Line1, s.Line2, lonEvalTime);
|
|
1876
|
-
aResult.longitude = (sLonAndDrift.longitude + 360) % 360; // Normalize to 0-360
|
|
1877
1989
|
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
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
|
-
}
|
|
1990
|
+
if (distKm < aResult.poca) {
|
|
1991
|
+
aResult.poca = distKm;
|
|
1992
|
+
aResult.toca = new Date(prim.t).toISOString();
|
|
1993
|
+
aResult.tocaString = getTimeDifference(aResult.toca);
|
|
1911
1994
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
aResult.danger = (aResult.di === 0 || aResult.poca === 0)
|
|
1998
|
+
? 1000
|
|
1999
|
+
: 1 / (aResult.di * aResult.poca);
|
|
2000
|
+
return aResult;
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
const getGeoRpoData = (line1, line2, sats, startTime, endTime, lonTime, includeRIC=false) => {
|
|
2004
|
+
const results = [];
|
|
2005
|
+
const start = new Date(startTime).getTime();
|
|
2006
|
+
const end = new Date(endTime).getTime();
|
|
2007
|
+
const pEphem = prop({Line1: line1, Line2: line2}, start, end, 60000);
|
|
2008
|
+
if (!isDefined(pEphem) || pEphem.length === 0) {
|
|
2009
|
+
return results; // Primary may have re-entered the atmosphere
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const lonEvalTime = lonTime ? new Date(lonTime) : new Date(end);
|
|
2013
|
+
const pLonAndDrift = getLonAndDrift(line1, line2, lonEvalTime);
|
|
1915
2014
|
|
|
1916
|
-
|
|
2015
|
+
sats.forEach((s) => {
|
|
2016
|
+
const sEphem = prop({Line1: s.Line1, Line2: s.Line2}, start, end, 60000);
|
|
2017
|
+
const sLonAndDrift = getLonAndDrift(s.Line1, s.Line2, lonEvalTime);
|
|
2018
|
+
const row = assembleGeoRpoResult(s, pEphem, sEphem, pLonAndDrift, sLonAndDrift, includeRIC);
|
|
2019
|
+
if (isDefined(row)) {results.push(row);}
|
|
1917
2020
|
});
|
|
1918
2021
|
|
|
1919
2022
|
return results;
|
|
1920
2023
|
};
|
|
1921
2024
|
|
|
1922
|
-
|
|
1923
|
-
// Accuracy is 86400 pooints on a 360 circle (i.e. 0.00416 deg per second)
|
|
2025
|
+
// `getGeoRpoDataBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
1924
2026
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2027
|
+
/** GEO mean-anomaly TLE template used by the shadow-zone scan. The
|
|
2028
|
+
* `XXX.XXXX` token is substituted with each candidate mean anomaly.
|
|
2029
|
+
* TLE lines are fixed-length by definition, hence the disable-next-line. */
|
|
2030
|
+
// eslint-disable-next-line max-len
|
|
2031
|
+
const SHADOW_ZONE_TLE_LINE_1 = "1 00000U 00000A 24079.98445361 -.00000000 00000-0 -00000-0 0 00000";
|
|
2032
|
+
// eslint-disable-next-line max-len
|
|
2033
|
+
const SHADOW_ZONE_TLE_LINE_2_TEMPLATE = "2 00000 000.0000 000.0000 0000000 000.0000 XXX.XXXX 01.00000000000000";
|
|
2034
|
+
|
|
2035
|
+
/**
|
|
2036
|
+
* Build the array of GEO mean-anomaly elsets the shadow-zone scan
|
|
2037
|
+
* exercises. Pure; no propagation. Produces `360 / accuracySecondsDeg`
|
|
2038
|
+
* elsets, one for each mean anomaly slot, in increasing order.
|
|
2039
|
+
*
|
|
2040
|
+
* @param {Number} accuracySecondsDeg degrees per slot
|
|
2041
|
+
* @return {Array<{Line1:string, Line2:string}>}
|
|
2042
|
+
*/
|
|
2043
|
+
const buildShadowZoneElsets = (accuracySecondsDeg) => {
|
|
2044
|
+
// Use Math.floor so fractional `360 / accuracySecondsDeg` (e.g.
|
|
2045
|
+
// accuracy values that don't divide 360 exactly) still produce a
|
|
2046
|
+
// valid integer step count. Matches the original loop's `i < steps`
|
|
2047
|
+
// behavior, which also truncated.
|
|
2048
|
+
const steps = Math.floor(360 / accuracySecondsDeg);
|
|
2049
|
+
const out = new Array(steps);
|
|
1932
2050
|
for (let i = 0; i < steps; i++) {
|
|
1933
|
-
// Calculate the mean anomaly for this step
|
|
1934
2051
|
const meanAnomaly = i * accuracySecondsDeg;
|
|
1935
|
-
// Format the mean anomaly into the TLE line
|
|
1936
|
-
// Round the mean anomaly to two decimal places
|
|
1937
2052
|
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
|
-
});
|
|
2053
|
+
const tleLine2 = SHADOW_ZONE_TLE_LINE_2_TEMPLATE.replace(
|
|
2054
|
+
"XXX.XXXX", roundedMeanAnomaly.padStart(8, "0"),
|
|
2055
|
+
);
|
|
2056
|
+
out[i] = {Line1: SHADOW_ZONE_TLE_LINE_1, Line2: tleLine2};
|
|
1946
2057
|
}
|
|
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
|
-
});
|
|
2058
|
+
return out;
|
|
2059
|
+
};
|
|
1955
2060
|
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2061
|
+
/**
|
|
2062
|
+
* Pure zone-analysis pass. Takes the per-slot
|
|
2063
|
+
* `[{ecl, geolon}, ...]` series produced by either the JS or WASM scan
|
|
2064
|
+
* and returns `{penStartWestLon, penStartEastLon}` exactly as the
|
|
2065
|
+
* original `getGeoShadowZones` does.
|
|
2066
|
+
*
|
|
2067
|
+
* Lifted out of the original function body without semantic changes:
|
|
2068
|
+
* - identical interval seam-stitching across the 360°/0° wraparound,
|
|
2069
|
+
* - identical "fully lit GEO belt" early return,
|
|
2070
|
+
* - identical penumbra-only vs. umbra+penumbra branching.
|
|
2071
|
+
*
|
|
2072
|
+
* @param {Array<{ecl:string, geolon:number}>} res
|
|
2073
|
+
* @return {{penStartWestLon:?number, penStartEastLon:?number}}
|
|
2074
|
+
*/
|
|
2075
|
+
const analyzeShadowZoneSeries = (res) => {
|
|
2076
|
+
const ints = [{ecl: res[0].ecl}];
|
|
1961
2077
|
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
});
|
|
2078
|
+
for (let i = 1; i <= res.length - 1; i++) {
|
|
2079
|
+
if (res[i].ecl !== res[i - 1].ecl) {
|
|
2080
|
+
ints.at(-1).stopDeg = res[i - 1].geolon;
|
|
2081
|
+
ints.push({ecl: res[i].ecl, startDeg: res[i].geolon});
|
|
1967
2082
|
}
|
|
1968
2083
|
}
|
|
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
2084
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
&& ints.filter((x)=>x.ecl === "SUN").length===1) {
|
|
1977
|
-
return {
|
|
1978
|
-
penStartWestLon: null,
|
|
1979
|
-
penStartEastLon: null,
|
|
1980
|
-
};
|
|
2085
|
+
if (ints.length === 1
|
|
2086
|
+
&& ints.filter((x) => x.ecl === "SUN").length === 1) {
|
|
2087
|
+
return {penStartWestLon: null, penStartEastLon: null};
|
|
1981
2088
|
}
|
|
1982
2089
|
|
|
1983
2090
|
ints[0].startDeg = ints.at(-1).startDeg;
|
|
1984
2091
|
ints.pop();
|
|
1985
2092
|
|
|
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");
|
|
2093
|
+
if (ints.filter((x) => x.ecl === "UMBRA").length === 0
|
|
2094
|
+
&& ints.some((x) => x.ecl === "PENUMBRA")) {
|
|
2095
|
+
const penumbraInt = ints.find((x) => x.ecl === "PENUMBRA");
|
|
1993
2096
|
return {
|
|
1994
2097
|
penStartWestLon: wrapToRange(penumbraInt.startDeg, 0, 360),
|
|
1995
2098
|
penStartEastLon: wrapToRange(penumbraInt.stopDeg, 0, 360),
|
|
1996
2099
|
};
|
|
1997
|
-
}
|
|
1998
|
-
// If sun, umbra, penumbra intervals exist
|
|
1999
|
-
|
|
2000
|
-
const umbra = ints.find((x)=>x.ecl === "UMBRA");
|
|
2100
|
+
}
|
|
2001
2101
|
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2102
|
+
const umbra = ints.find((x) => x.ecl === "UMBRA");
|
|
2103
|
+
const umbraStart360 = wrapToRange(umbra.startDeg, 0, 360);
|
|
2104
|
+
const umbraStop360 = wrapToRange(umbra.stopDeg, 0, 360);
|
|
2105
|
+
const penumbraInts = ints.filter((x) => x.ecl === "PENUMBRA");
|
|
2106
|
+
const chosenPenumbraInt = penumbraInts[0].startDeg > penumbraInts[0].stopDeg
|
|
2107
|
+
? penumbraInts[1] : penumbraInts[0];
|
|
2108
|
+
const penumbraAngle = chosenPenumbraInt.stopDeg - chosenPenumbraInt.startDeg;
|
|
2109
|
+
return {
|
|
2110
|
+
penStartWestLon: umbraStart360 - penumbraAngle,
|
|
2111
|
+
penStartEastLon: umbraStop360 + penumbraAngle,
|
|
2112
|
+
};
|
|
2113
|
+
};
|
|
2012
2114
|
|
|
2115
|
+
const getGeoShadowZones = (time, accuracySecondsDeg = 0.00416 * 100) => {
|
|
2116
|
+
// Default accuracy: 86400 points on the 360° circle (~0.00416 deg/sec).
|
|
2117
|
+
const elsets = buildShadowZoneElsets(accuracySecondsDeg);
|
|
2118
|
+
const res = elsets.map(({Line1, Line2}) => {
|
|
2119
|
+
const p = propagate(twoline2satrec(Line1, Line2), time).position;
|
|
2013
2120
|
return {
|
|
2014
|
-
|
|
2015
|
-
|
|
2121
|
+
ecl: getEclipseStatus(time, [p.x, p.y, p.z]),
|
|
2122
|
+
geolon: eciToGeodetic(p, gstime(time)).longitude * RAD2DEG,
|
|
2016
2123
|
};
|
|
2017
|
-
}
|
|
2124
|
+
});
|
|
2125
|
+
return analyzeShadowZoneSeries(res);
|
|
2018
2126
|
};
|
|
2019
2127
|
|
|
2128
|
+
// `getGeoShadowZonesBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
2129
|
+
|
|
2020
2130
|
/** Returns the light intervals of an arbitrary satellite in strict GEO orbit.
|
|
2021
2131
|
*
|
|
2022
2132
|
* These intervals are generally equal in duration for all other GEO satellites.
|
|
@@ -2415,66 +2525,85 @@ const calculateLeoPhaseDifference = (pLine1, pLine2, tLine1, tLine2, time) => {
|
|
|
2415
2525
|
* @param {Integer} stepMs, step time of the analysis in milliseconds, default 10s
|
|
2416
2526
|
* @return {Array} Array of data objects
|
|
2417
2527
|
*/
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2528
|
+
/**
|
|
2529
|
+
* Build the breakpoint schedule for `getLeoWaterfallData` /
|
|
2530
|
+
* `getLeoWaterfallDataBulk`. The waterfall covers `[start, end]` divided
|
|
2531
|
+
* into segments at every per-satellite elset epoch that falls strictly
|
|
2532
|
+
* inside that range. Pure; no propagation.
|
|
2533
|
+
*
|
|
2534
|
+
* Each breakpoint is `{time, satIndex, elsetIndex?}`. The two synthetic
|
|
2535
|
+
* sentinel breakpoints at `start` / `end` carry `satIndex: -1` so the
|
|
2536
|
+
* segment loop can distinguish "edge of window" from "satellite epoch".
|
|
2537
|
+
*
|
|
2538
|
+
* @param {Array<Array<Object>>} elsets per-satellite elset arrays
|
|
2539
|
+
* @param {Number} start Unix ms
|
|
2540
|
+
* @param {Number} end Unix ms
|
|
2541
|
+
* @return {Array<{time:number, satIndex:number, elsetIndex?:number}>}
|
|
2542
|
+
*/
|
|
2543
|
+
const buildWaterfallBreakpoints = (elsets, start, end) => {
|
|
2425
2544
|
const breakpoints = [{time: start, satIndex: -1}, {time: end, satIndex: -1}];
|
|
2426
2545
|
elsets.forEach((satElsets, i) => {
|
|
2427
2546
|
breakpoints.push(...satElsets.map((e, j) => ({
|
|
2428
2547
|
time: new Date(e.Epoch).getTime(),
|
|
2429
2548
|
satIndex: i,
|
|
2430
2549
|
elsetIndex: j,
|
|
2431
|
-
})).filter((b) => b.time > start && b.time < end));
|
|
2550
|
+
})).filter((b) => b.time > start && b.time < end));
|
|
2432
2551
|
});
|
|
2433
2552
|
breakpoints.sort((a, b) => a.time - b.time);
|
|
2553
|
+
return breakpoints;
|
|
2554
|
+
};
|
|
2434
2555
|
|
|
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
|
-
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Decorate a per-segment ephemeris with osculating elements and elset
|
|
2558
|
+
* metadata. Identical post-processing as the original inline `.map()` in
|
|
2559
|
+
* `getLeoWaterfallData`. Used by both the JS and WASM waterfall variants
|
|
2560
|
+
* so the two emit byte-equivalent rows.
|
|
2561
|
+
*
|
|
2562
|
+
* `isEpoch` is `true` for the first point of the segment if-and-only-if
|
|
2563
|
+
* the segment was opened by *this* satellite's elset epoch (matches the
|
|
2564
|
+
* original `pointInd === 0 && bkpoint.satIndex === satIndex` check).
|
|
2565
|
+
*
|
|
2566
|
+
* @param {Array<{p:number[],v:number[],t:number}>} ephem segment ephemeris
|
|
2567
|
+
* @param {Object} elset active elset for this satellite in this segment
|
|
2568
|
+
* @param {{satIndex:number}} bkpoint segment-opening breakpoint
|
|
2569
|
+
* @param {Number} satIndex index of the satellite in the original `elsets`
|
|
2570
|
+
* @return {Array<Object>}
|
|
2571
|
+
*/
|
|
2572
|
+
const decorateWaterfallSegment = (ephem, elset, bkpoint, satIndex) => {
|
|
2573
|
+
const epoch = new Date(elset.Epoch).getTime();
|
|
2574
|
+
return ephem.map((point, pointInd) => {
|
|
2575
|
+
const osculatingElements = cartesianToKeplerian(
|
|
2576
|
+
multiply(posToArray(point.p), 1000), // km -> m
|
|
2577
|
+
multiply(posToArray(point.v), 1000), // km/s -> m/s
|
|
2578
|
+
);
|
|
2579
|
+
return {
|
|
2580
|
+
...point,
|
|
2581
|
+
isEpoch: pointInd === 0 && bkpoint.satIndex === satIndex,
|
|
2582
|
+
epoch: epoch,
|
|
2583
|
+
satNo: elset.SatNo,
|
|
2584
|
+
name: elset.CommonName,
|
|
2585
|
+
source: elset.Source,
|
|
2586
|
+
raan: osculatingElements.raan, // deg
|
|
2587
|
+
raanPrecession: elset.RaanPrecessionDegreesPerDay,
|
|
2588
|
+
inclination: osculatingElements.i, // deg
|
|
2589
|
+
};
|
|
2590
|
+
});
|
|
2591
|
+
};
|
|
2473
2592
|
|
|
2593
|
+
/**
|
|
2594
|
+
* Build the per-(time, satellite) waterfall result rows from the
|
|
2595
|
+
* decorated ephemerides produced by {@link decorateWaterfallSegment}.
|
|
2596
|
+
* Identical math to the second loop of the original `getLeoWaterfallData`.
|
|
2597
|
+
* Pure; deterministic; shared by both variants.
|
|
2598
|
+
*
|
|
2599
|
+
* @param {Array<Array<Object>>} satEphems primary at index 0
|
|
2600
|
+
* @return {Array<Object>}
|
|
2601
|
+
*/
|
|
2602
|
+
const assembleWaterfallRows = (satEphems) => {
|
|
2603
|
+
const results = [];
|
|
2474
2604
|
const pEphem = satEphems[0];
|
|
2475
2605
|
|
|
2476
|
-
satEphems.forEach(
|
|
2477
|
-
// Find the distance at each time step
|
|
2606
|
+
satEphems.forEach((sEphem, satIndex) => {
|
|
2478
2607
|
for (let i = 0; i < pEphem.length; i++) {
|
|
2479
2608
|
const pEphemPoint = pEphem[i];
|
|
2480
2609
|
const sEphemPoint = sEphem[i];
|
|
@@ -2493,7 +2622,7 @@ const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
|
2493
2622
|
|
|
2494
2623
|
if (satIndex > 0) { // not the primary
|
|
2495
2624
|
if (areCoordsEqual(pEphemPoint.p, sEphemPoint.p)) {
|
|
2496
|
-
catsAngle = 180;
|
|
2625
|
+
catsAngle = 180;
|
|
2497
2626
|
} else {
|
|
2498
2627
|
catsAngle = angleBetween3DCoords(
|
|
2499
2628
|
pEphemPoint.p,
|
|
@@ -2501,7 +2630,6 @@ const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
|
2501
2630
|
getSunDirection(time),
|
|
2502
2631
|
);
|
|
2503
2632
|
}
|
|
2504
|
-
|
|
2505
2633
|
const earthEclipsed = doesLineSegmentIntersectEarth(pEphemPoint.p, sEphemPoint.p);
|
|
2506
2634
|
const targetShadow = getEclipseStatus(time, posToArray(sEphemPoint.p));
|
|
2507
2635
|
targetVisibility = earthEclipsed ? "EARTH ECLIPSED" : targetShadow;
|
|
@@ -2529,6 +2657,40 @@ const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
|
2529
2657
|
return results;
|
|
2530
2658
|
};
|
|
2531
2659
|
|
|
2660
|
+
const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
2661
|
+
const start = new Date(startTime).getTime();
|
|
2662
|
+
const end = new Date(endTime).getTime();
|
|
2663
|
+
|
|
2664
|
+
const breakpoints = buildWaterfallBreakpoints(elsets, start, end);
|
|
2665
|
+
const currentIndices = elsets.map(() => 0);
|
|
2666
|
+
const satEphems = elsets.map(() => []);
|
|
2667
|
+
|
|
2668
|
+
for (let i = 0; i < breakpoints.length - 1; i++) {
|
|
2669
|
+
const bkpoint = breakpoints[i];
|
|
2670
|
+
// Segment boundaries align with elset epochs, not multiples of `stepMs`.
|
|
2671
|
+
// The intra-segment ephemeris drift (<= one step) is negligible at the
|
|
2672
|
+
// multi-day plot resolution this function targets.
|
|
2673
|
+
const segmentStart = bkpoint.time;
|
|
2674
|
+
const segmentEnd = breakpoints[i + 1].time;
|
|
2675
|
+
|
|
2676
|
+
if (bkpoint.satIndex >= 0) {
|
|
2677
|
+
currentIndices[bkpoint.satIndex] = bkpoint.elsetIndex;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
currentIndices.forEach((elsetIndex, satIndex) => {
|
|
2681
|
+
const elset = elsets[satIndex][elsetIndex];
|
|
2682
|
+
const ephem = prop(elset, segmentStart, segmentEnd, stepMs);
|
|
2683
|
+
satEphems[satIndex].push(
|
|
2684
|
+
...decorateWaterfallSegment(ephem, elset, bkpoint, satIndex),
|
|
2685
|
+
);
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
return assembleWaterfallRows(satEphems);
|
|
2690
|
+
};
|
|
2691
|
+
|
|
2692
|
+
// `getLeoWaterfallDataBulk` extracted to `./wasmProp/wasmAstro.js`.
|
|
2693
|
+
|
|
2532
2694
|
/**
|
|
2533
2695
|
* Returns the minimum deltaV for a Lambert maneuver between two satellites,
|
|
2534
2696
|
* within the given time range.
|
|
@@ -3325,6 +3487,18 @@ export {
|
|
|
3325
3487
|
getGeoRpoData,
|
|
3326
3488
|
getGeoShadowZones,
|
|
3327
3489
|
getGeoLightIntervals,
|
|
3490
|
+
// Shared pure assembly helpers, exported so `./wasmProp/wasmAstro.js` can
|
|
3491
|
+
// consume them without duplicating logic. These are cross-path
|
|
3492
|
+
// building blocks (used by both the JS-sync and WASM-async siblings)
|
|
3493
|
+
// and are safe to call as ordinary utilities.
|
|
3494
|
+
assembleLeoRpoResult,
|
|
3495
|
+
geoRelativeLonDrift,
|
|
3496
|
+
assembleGeoRpoResult,
|
|
3497
|
+
buildShadowZoneElsets,
|
|
3498
|
+
analyzeShadowZoneSeries,
|
|
3499
|
+
buildWaterfallBreakpoints,
|
|
3500
|
+
decorateWaterfallSegment,
|
|
3501
|
+
assembleWaterfallRows,
|
|
3328
3502
|
getEclipseStatus,
|
|
3329
3503
|
estimateSlantRange,
|
|
3330
3504
|
calculateNextApogeePerigeeTimes,
|