Skip to content

Instantly share code, notes, and snippets.

@tuckergordon
Last active January 1, 2020 08:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tuckergordon/ce135a88cd14991761ccdc937179c6c0 to your computer and use it in GitHub Desktop.
Save tuckergordon/ce135a88cd14991761ccdc937179c6c0 to your computer and use it in GitHub Desktop.
Satellite Footprints on Globe Using D3

3D Globe version of Satellite Footprints Using Leaflet + D3. Only uses D3, not Leaflet or Three.js Based off of Brian Swedberg's block.

Projects the conical footprints of satellites on a globe rendered by D3. Each satellite is assigned a random half angle that defines a conical field of view from its position at a certain time.

As with Brian's block, this maps 3 different types of satellites:

  • Geosynchronous orbits (GEO) and potentially geostationary; high altitude
  • Medium-Earth orbits (MEO); mid altitude
  • Low-Earth orbits (LEO); low altitude

Resources and Key Libs:

Data

Space-Track: Latest curated weather satellites on 2018 JAN 26

<!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, 0, 0, 0.5);
stroke: rgba(255, 0, 0, 0.5);
}
.footprint--MEO {
fill: rgba(0, 255, 0, 0.5);
stroke: rgba(0, 255, 0, 0.5);
}
.footprint--GEO {
fill: rgba(0, 0, 255, 0.5);
stroke: rgba(0, 0, 255, 0.5);
}
</style>
</head>
<body>
<svg id="globe" width="960" height="500"></svg>
<script src="index.js"></script>
</body>
</html>
(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('text')
.attr('class', 'date-counter')
.attr('x', 10)
.attr('y', 20)
.attr('height', 20)
.attr('fill', '#222222');
svg.append('path')
.datum({
type: 'Sphere'
})
.style('cursor', 'grab')
.attr('fill', 'lightblue')
.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', '.6');
});
}
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(1000)
.date(TLE_DATA_DATE);
sats = parsedTles.map(function (tle) {
var sat = new Satellite(tle, new Date(2018, 0, 26));
sat.halfAngle(sat.getOrbitType() === 'LEO' ? Math.random() * (30 - 15) + 15 : Math.random() * 4 + 1);
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();
d3.text('tles.txt')
.then(parseTle)
.then(initSats);
}(window.L, window.d3, window.satellite))
Satellite Footprints on Globe Using D3
1 13367U 82072A 19025.67382660 .00000448 00000-0 26967-4 0 9994
2 13367 98.2368 249.8468 0013264 8.7955 32.7108 15.16610100974056
1 14780U 84021A 19025.86363710 .00000116 00000-0 16831-4 0 9997
2 14780 98.0997 161.4728 0091105 36.1375 324.5953 14.91839885863480
1 23327U 94069A 19026.00535573 -.00000103 00000-0 00000+0 0 9995
2 23327 15.0768 11.9671 0006229 236.5550 311.7352 1.00256785 88794
1 23435U 94084A 19024.83932884 -.00000312 +00000-0 +00000-0 0 9990
2 23435 014.4382 016.6476 0000815 332.2555 027.7724 01.00274534005886
1 23522U 95011B 19024.67560573 -.00000079 +00000-0 +00000-0 0 9992
2 23522 013.0828 024.6289 0004086 030.5208 140.5982 00.99408123086756
1 24737U 97008A 19025.69976709 .00000015 00000-0 00000+0 0 9992
2 24737 13.2809 24.2633 0000764 351.4157 8.6214 1.00276141 4547
1 24883U 97037A 19025.92104745 .00000048 00000-0 33196-4 0 9998
2 24883 98.5737 137.9339 0002915 147.7438 212.3925 14.33487628136114
1 25682U 99020A 19026.06863801 -.00000075 00000-0 -66221-5 0 9999
2 25682 98.1462 96.7829 0001514 66.1136 294.0226 14.57146428 52206
1 26356U 00024A 19025.19833423 .00000086 00000-0 00000+0 0 9991
2 26356 11.1219 30.2370 0001922 243.8369 116.1978 1.00260483 4532
1 26382U 00032A 19024.86851399 -.00000268 +00000-0 +00000-0 0 9998
2 26382 010.6166 035.5053 0006959 274.2378 095.2784 01.00100162067904
1 26880U 01033A 19025.00737590 -.00000229 00000-0 00000+0 0 9992
2 26880 10.2805 33.8794 0001808 264.1991 95.8416 1.00565296 4545
1 27424U 02022A 19025.84727490 .00000068 00000-0 25207-4 0 9997
2 27424 98.2200 328.7722 0002723 100.8342 352.8074 14.57105658889854
1 27431U 02024B 19026.09716593 .00000072 00000-0 67455-4 0 9993
2 27431 99.1030 12.3417 0014560 240.7019 119.2696 14.09668664859239
1 27509U 02040B 19024.69653201 +.00000124 +00000-0 +00000-0 0 9994
2 27509 005.8452 056.1597 0001724 293.7646 066.2974 01.00260509060185
1 27640U 03001A 19025.89096844 -.00000021 00000-0 10909-4 0 9995
2 27640 98.7351 37.8806 0015021 61.0452 1.9643 14.18999185831294
1 28158U 04004A 19024.53176755 -.00000230 +00000-0 +00000-0 0 9993
2 28158 008.5150 042.1647 0000433 351.4919 008.5823 01.00275577005859
1 28451U 04042A 19024.90253603 -.00000199 +00000-0 +00000-0 0 9994
2 28451 008.2492 047.2507 0008246 287.9961 252.7179 00.98084409014688
1 28622U 05006A 19024.93728510 -.00000212 +00000-0 +00000-0 0 9998
2 28622 003.0450 080.3576 0020170 232.3197 100.3733 00.98913816017303
1 35491U 09033A 19025.94215816 -.00000107 00000-0 00000+0 0 9998
2 35491 0.0497 263.7297 0010165 338.7681 117.3692 1.00270541 35095
1 35865U 09049A 19026.07608278 .00000016 00000-0 25779-4 0 9996
2 35865 98.4084 30.8150 0001574 208.8813 151.2279 14.22181546485666
1 36585U 10022A 19025.89089603 -.00000068 00000-0 00000+0 0 9996
2 36585 55.6744 317.2611 0079151 47.6762 128.7805 2.00553159 63444
1 37849U 11061A 19025.85736228 .00000007 00000-0 23947-4 0 9994
2 37849 98.7234 326.0892 0000565 70.9996 354.7392 14.19555238375463
1 38552U 12035B 19024.63072767 +.00000048 +00000-0 +00000-0 0 9990
2 38552 000.9893 007.1089 0001363 293.7860 059.0647 01.00274915023811
1 38771U 12049A 19025.86637133 -.00000017 00000-0 12164-4 0 9993
2 38771 98.7252 87.0576 0001546 57.9352 30.5283 14.21479598329769
1 38833U 12053A 19023.02160365 .00000037 00000-0 00000+0 0 9993
2 38833 53.8355 254.4252 0079636 32.0767 328.4833 2.00556310 46144
1 39166U 13023A 19024.88023431 -.00000069 +00000-0 +00000-0 0 9995
2 39166 056.0598 016.9330 0064614 022.3880 337.9012 02.00568602041724
1 39260U 13052A 19026.16721979 -.00000003 00000-0 20289-4 0 9990
2 39260 98.6256 89.8045 0010154 232.1561 127.8704 14.15859509276699
1 39533U 14008A 19024.37829364 +.00000016 +00000-0 +00000-0 0 9991
2 39533 053.9711 259.7478 0036433 190.6262 169.3678 02.00567695035534
1 39741U 14026A 19024.16378102 -.00000041 00000-0 00000+0 0 9996
2 39741 55.7949 76.9733 0016044 288.6136 71.2806 2.00551076 34343
1 40105U 14045A 19024.74875596 +.00000061 +00000-0 +00000-0 0 9998
2 40105 054.5792 196.3261 0012539 098.7053 261.4161 02.00554590031942
1 40267U 14060A 19025.26401613 -.00000289 00000-0 00000+0 0 9999
2 40267 0.0201 68.4083 0001415 265.0675 26.5170 1.00270026 15796
1 40294U 14068A 19024.12109425 +.00000009 +00000-0 +00000-0 0 9997
2 40294 055.1331 137.1864 0017513 032.1169 327.9794 02.00572545031036
1 40534U 15013A 19024.76324260 -.00000059 +00000-0 +00000-0 0 9997
2 40534 054.6078 315.7983 0035536 002.2241 357.8177 02.00550117027667
1 40730U 15033A 19024.41704627 -.00000070 +00000-0 +00000-0 0 9991
2 40730 055.5866 016.3563 0042635 335.7673 024.0406 02.00562368025846
1 40732U 15034A 19024.65436339 -.00000008 +00000-0 +00000-0 0 9993
2 40732 000.9909 229.8338 0001690 110.3177 019.8112 01.00282602012967
1 41866U 16071A 19025.86201983 -.00000262 00000-0 00000+0 0 9994
2 41866 0.0169 116.2286 0000618 233.8853 9.8553 1.00271331 8046
1 43013U 17073A 19025.95010176 .00000007 00000-0 24241-4 0 9999
2 43013 98.7491 326.2096 0001018 62.2366 297.8913 14.19537735 61511
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment