This commit is contained in:
John Doe 2021-04-29 00:05:01 -04:00
commit 8f98aa225e
7 changed files with 669 additions and 0 deletions

398
sats/index.js Normal file
View file

@ -0,0 +1,398 @@
(function (L, d3, satelliteJs) {
var RADIANS = Math.PI / 180;
var DEGREES = 180 / Math.PI;
var R_EARTH = 6378.137; // equatorial radius (km)
/* =============================================== */
/* =============== CLOCK ========================= */
/* =============================================== */
/**
* Factory function for keeping track of elapsed time and rates.
*/
function Clock() {
this._rate = 60; // 1ms elapsed : 60sec simulated
this._date = d3.now();
this._elapsed = 0;
};
Clock.prototype.date = function (timeInMs) {
if (!arguments.length) return this._date + (this._elapsed * this._rate);
this._date = timeInMs;
return this;
};
Clock.prototype.elapsed = function (ms) {
if (!arguments.length) return this._date - d3.now(); // calculates elapsed
this._elapsed = ms;
return this;
};
Clock.prototype.rate = function (secondsPerMsElapsed) {
if (!arguments.length) return this._rate;
this._rate = secondsPerMsElapsed;
return this;
};
/* ==================================================== */
/* =============== CONVERSION ========================= */
/* ==================================================== */
function satrecToFeature(satrec, date, props) {
var properties = props || {};
var positionAndVelocity = satelliteJs.propagate(satrec, date);
var gmst = satelliteJs.gstime(date);
var positionGd = satelliteJs.eciToGeodetic(positionAndVelocity.position, gmst);
properties.height = positionGd.height;
return {
type: 'Feature',
properties: properties,
geometry: {
type: 'Point',
coordinates: [
positionGd.longitude * DEGREES,
positionGd.latitude * DEGREES
]
}
};
};
/* ==================================================== */
/* =============== TLE ================================ */
/* ==================================================== */
/**
* Factory function for working with TLE.
*/
function TLE() {
this._properties;
this._date;
};
TLE.prototype._lines = function (arry) {
return arry.slice(0, 2);
};
TLE.prototype.satrecs = function (tles) {
return tles.map(function (d) {
return satelliteJs.twoline2satrec.apply(null, this._lines(d));
});
};
TLE.prototype.features = function (tles) {
var date = this._date || d3.now();
return tles.map(function (d) {
var satrec = satelliteJs.twoline2satrec.apply(null, this._lines(d));
return satrecToFeature(satrec, date, this._properties(d));
});
};
TLE.prototype.lines = function (func) {
if (!arguments.length) return this._lines;
this._lines = func;
return this;
};
TLE.prototype.properties = function (func) {
if (!arguments.length) return this._properties;
this._properties = func;
return this;
};
TLE.prototype.date = function (ms) {
if (!arguments.length) return this._date;
this._date = ms;
return this;
};
/* ==================================================== */
/* =============== PARSE ============================== */
/* ==================================================== */
/**
* Parses text file string of tle into groups.
* @return {string[][]} Like [['tle line 1', 'tle line 2'], ...]
*/
function parseTle(tleString) {
// remove last newline so that we can properly split all the lines
var lines = tleString.replace(/\r?\n$/g, '').split(/\r?\n/);
return lines.reduce(function (acc, cur, index) {
if (index % 2 === 0) acc.push([]);
acc[acc.length - 1].push(cur);
return acc;
}, []);
};
/* ==================================================== */
/* =============== SATELLITE ========================== */
/* ==================================================== */
/**
* Satellite factory function that wraps satellitejs functionality
* and can compute footprints based on TLE and date
*
* @param {string[][]} tle two-line element
* @param {Date} date date to propagate with TLE
*/
function Satellite(tle, date) {
this._satrec = satelliteJs.twoline2satrec(tle[0], tle[1]);
this._satNum = this._satrec.satnum; // NORAD Catalog Number
this._altitude; // km
this._position = {
lat: null,
lng: null
};
this._halfAngle; // degrees
this._date;
this._gmst;
this.setDate(date);
this.update();
this._orbitType = this.orbitTypeFromAlt(this._altitude); // LEO, MEO, or GEO
};
/**
* Updates satellite position and altitude based on current TLE and date
*/
Satellite.prototype.update = function () {
var positionAndVelocity = satelliteJs.propagate(this._satrec, this._date);
var positionGd = satelliteJs.eciToGeodetic(positionAndVelocity.position, this._gmst);
this._position = {
lat: positionGd.latitude * DEGREES,
lng: positionGd.longitude * DEGREES
};
this._altitude = positionGd.height;
return this;
};
/**
* @returns {GeoJSON.Polygon} GeoJSON describing the satellite's current footprint on the Earth
*/
Satellite.prototype.getFootprint = function () {
var theta = this._halfAngle * RADIANS;
coreAngle = this._coreAngle(theta, this._altitude, R_EARTH) * DEGREES;
return d3.geoCircle()
.center([this._position.lng, this._position.lat])
.radius(coreAngle)();
};
/**
* A conical satellite with half angle casts a circle on the Earth. Find the angle
* from the center of the earth to the radius of this circle
* @param {number} theta: Satellite half angle in radians
* @param {number} altitude Satellite altitude
* @param {number} r Earth radius
* @returns {number} core angle in radians
*/
Satellite.prototype._coreAngle = function (theta, altitude, r) {
// if FOV is larger than Earth, assume it goes to the tangential point
if (Math.sin(theta) > r / (altitude + r)) {
return Math.acos(r / (r + altitude));
}
return Math.abs(Math.asin((r + altitude) * Math.sin(theta) / r)) - theta;
};
Satellite.prototype.halfAngle = function (halfAngle) {
if (!arguments.length) return this._halfAngle;
this._halfAngle = halfAngle;
return this;
};
Satellite.prototype.satNum = function (satNum) {
if (!arguments.length) return this._satNum;
this._satNum = satNum;
return this;
};
Satellite.prototype.altitude = function (altitude) {
if (!arguments.length) return this._altitude;
this._altitude = altitude;
return this;
};
Satellite.prototype.position = function (position) {
if (!arguments.length) return this._position;
this._position = position;
return this;
};
Satellite.prototype.getOrbitType = function () {
return this._orbitType;
};
/**
* sets both the date and the Greenwich Mean Sidereal Time
* @param {Date} date
*/
Satellite.prototype.setDate = function (date) {
this._date = date;
this._gmst = satelliteJs.gstime(date);
return this;
};
/**
* Maps an altitude to a type of satellite
* @param {number} altitude (in KM)
* @returns {'LEO' | 'MEO' | 'GEO'}
*/
Satellite.prototype.orbitTypeFromAlt = function (altitude) {
this._altitude = altitude || this._altitude;
return this._altitude < 1200 ? 'LEO' : this._altitude > 22000 ? 'GEO' : 'MEO';
};
/* =============================================== */
/* =================== GLOBE ===================== */
/* =============================================== */
// Approximate date the tle data was aquired from https://www.space-track.org/#recent
var TLE_DATA_DATE = new Date(2018, 0, 26).getTime();
var activeClock;
var sats;
var svg = d3.select('#globe');
var marginTop = 20;
var width = svg.attr('width');
var height = svg.attr('height') - marginTop;
var projection = d3.geoOrthographic()
.scale((height - 10) / 2)
.translate([width / 2, height / 2 + marginTop])
.precision(0.1);
var geoPath = d3.geoPath()
.projection(projection);
svg.append('path')
.datum({
type: 'Sphere'
})
.style('cursor', 'grab')
.attr('fill', '#2E86AB')
.attr('d', geoPath);
function initGlobe() {
d3.json('world-110m.json').then(function (worldData) {
svg.selectAll('.segment')
.data(topojson.feature(worldData, worldData.objects.countries).features)
.enter().append('path')
.style('cursor', 'grab')
.attr('class', 'segment')
.attr('d', geoPath)
.style('stroke', '#888')
.style('stroke-width', '1px')
.style('fill', '#e5e5e5')
.style('opacity', '.4');
});
}
function updateSats(date) {
sats.forEach(function (sat) {
return sat.setDate(date).update()
});
return sats
};
/**
* Create satellite objects for each record in the TLEs and begin animation
* @param {string[][]} parsedTles
*/
function initSats(parsedTles) {
activeClock = new Clock()
.rate(100)
.date(TLE_DATA_DATE);
sats = parsedTles.map(function (tle) {
var sat = new Satellite(tle, new Date(2018, 0, 26));
//61.73 degree half angle corresponds to 3 degree coverage angle, obviously
//this is far less than we intend to provide but it works well for this graphic.
sat.halfAngle(61.73);
return sat;
});
window.requestAnimationFrame(animateSats);
return sats;
};
function draw() {
redrawGlobe();
svg.selectAll('.footprint')
.data(sats, function (sat) {
return sat.satNum();
})
.join(
function (enter) {
return enter.append('path')
.attr('class', function (sat) {
return 'footprint footprint--' + sat.getOrbitType()
})
.style('cursor', 'grab');
}
).attr('d', function (sat) {
return geoPath(sat.getFootprint());
});
};
function redrawGlobe() {
svg.selectAll('.segment')
.attr('d', geoPath);
}
var m0;
var o0;
function mousedown() {
m0 = [d3.event.pageX, d3.event.pageY];
o0 = projection.rotate();
d3.event.preventDefault();
};
function mousemove() {
if (m0) {
var m1 = [d3.event.pageX, d3.event.pageY];
const o1 = [o0[0] + (m1[0] - m0[0]) / 6, o0[1] + (m0[1] - m1[1]) / 6];
projection.rotate(o1);
draw();
}
};
function mouseup() {
if (m0) {
mousemove();
m0 = null;
}
}
svg.on('mousedown', mousedown);
d3.select(window)
.on('mousemove', mousemove)
.on('mouseup', mouseup);
function animateSats(elapsed) {
var dateInMs = activeClock.elapsed(elapsed)
.date();
var date = new Date(dateInMs);
svg.select('.date-counter').text('' + date);
updateSats(date);
draw();
window.requestAnimationFrame(animateSats);
}
initGlobe();
// WARNING: THE TLES HERE ARE NOT REAL NORAD-PUBLISHED TLES - THE TLE FORMAT
// IS USED ONLY AS A WAY TO SPECIFY THE ORBITS IN THE CONSTELLATION.
d3.text('tles.txt')
.then(parseTle)
.then(initSats);
}(window.L, window.d3, window.satellite))

29
sats/satellites.html Normal file
View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css" integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
crossorigin="" />
<script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js" integrity="sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg=="
crossorigin=""></script>
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8.1/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@3.0/dist/fetch.umd.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="https://d3js.org/d3-queue.v3.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/satellite.js/3.0.0/satellite.min.js"></script>
<style>
.footprint--LEO {
fill: rgba(255, 177, 64, 0.08);
stroke: rgba(255, 177, 64, 0.5);
}
</style>
</head>
<body style="margin: 0px; background-color: #00111d;">
<svg id="globe" width="500" height="500"></svg>
<script src="index.js"></script>
</body>
</html>

4
sats/tles-test.txt Normal file
View file

@ -0,0 +1,4 @@
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 000.0000 0000000 000.0000 000.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 000.0000 0000000 000.0000 050.6230 14.12748000000000

96
sats/tles.txt Normal file
View file

@ -0,0 +1,96 @@
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 000.0000 0000000 000.0000 000.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 000.0000 0000000 000.0000 090.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 000.0000 0000000 000.0000 180.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 000.0000 0000000 000.0000 270.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 030.0000 0000000 000.0000 307.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 030.0000 0000000 000.0000 037.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 030.0000 0000000 000.0000 127.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 030.0000 0000000 000.0000 217.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 060.0000 0000000 000.0000 255.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 060.0000 0000000 000.0000 345.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 060.0000 0000000 000.0000 075.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 060.0000 0000000 000.0000 165.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 090.0000 0000000 000.0000 202.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 090.0000 0000000 000.0000 292.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 090.0000 0000000 000.0000 022.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 090.0000 0000000 000.0000 112.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 120.0000 0000000 000.0000 150.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 120.0000 0000000 000.0000 240.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 120.0000 0000000 000.0000 330.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 120.0000 0000000 000.0000 060.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 150.0000 0000000 000.0000 097.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 150.0000 0000000 000.0000 187.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 150.0000 0000000 000.0000 277.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 150.0000 0000000 000.0000 007.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 180.0000 0000000 000.0000 045.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 180.0000 0000000 000.0000 135.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 180.0000 0000000 000.0000 225.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 180.0000 0000000 000.0000 315.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 210.0000 0000000 000.0000 352.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 210.0000 0000000 000.0000 082.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 210.0000 0000000 000.0000 172.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 210.0000 0000000 000.0000 262.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 240.0000 0000000 000.0000 300.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 240.0000 0000000 000.0000 030.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 240.0000 0000000 000.0000 120.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 240.0000 0000000 000.0000 210.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 270.0000 0000000 000.0000 247.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 270.0000 0000000 000.0000 337.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 270.0000 0000000 000.0000 067.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 270.0000 0000000 000.0000 157.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 300.0000 0000000 000.0000 195.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 300.0000 0000000 000.0000 285.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 300.0000 0000000 000.0000 015.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 300.0000 0000000 000.0000 105.0000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 330.0000 0000000 000.0000 142.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 330.0000 0000000 000.0000 232.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 330.0000 0000000 000.0000 322.5000 14.12748000000000
1 00000U 00000A 00000.00000000 .00000000 00000-0 00000-0 0 0000
2 00000 53.0000 330.0000 0000000 000.0000 052.5000 14.12748000000000

1
sats/world-110m.json Normal file

File diff suppressed because one or more lines are too long