Skip to content

Instantly share code, notes, and snippets.

@tuckergordon
Last active October 30, 2023 17:13
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/5cdd5cc346b6b1d1fd0120562ac23d5f to your computer and use it in GitHub Desktop.
Save tuckergordon/5cdd5cc346b6b1d1fd0120562ac23d5f to your computer and use it in GitHub Desktop.
Satellite Footprints Using Leaflet + D3

Based off of Brian Swedberg's block.

Updated to show the conical footprints of satellites. 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://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>
<div id="leaflet-map" style="width:960px;height:500px"></div>
<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';
};
/* =============================================== */
/* =============== LEAFLET MAP =================== */
/* =============================================== */
// 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 leafletMap;
var attributionControl;
var activeClock;
var sats;
var svgLayer;
function projectPointCurry(map) {
return function (x, y) {
const point = map.latLngToLayerPoint(L.latLng(y, x));
this.stream.point(point.x, point.y);
}
};
function init() {
svgLayer = L.svg();
leafletMap = L.map('leaflet-map', {
zoom: 2,
center: [20, 0],
attributionControl: false,
layers: [
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
noWrap: true,
bounds: [
[-90, -180],
[90, 180]
]
}),
svgLayer
]
});
attributionControl = L.control.attribution({
prefix: ''
}).addTo(leafletMap);
var transform = d3.geoTransform({
point: projectPointCurry(leafletMap)
});
path = d3.geoPath()
.projection(transform)
.pointRadius(2.5);
};
function updateSats(date) {
sats.forEach(function (sat) {
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;
});
leafletMap.on('zoom', draw);
window.requestAnimationFrame(animateSats);
return sats;
};
function invertProjection(projection) {
return function (x, y) {
const point = projection.invert([x, y]);
this.stream.point(point[0], point[1]);
};
}
function clipMercator(geoJson) {
const mercator = d3.geoMercator();
const inverseMercator = d3.geoTransform({
point: invertProjection(mercator)
});
// D3 geoProject handles Mercator clipping
const newJson = d3.geoProject(geoJson, mercator);
return d3.geoProject(newJson, inverseMercator);
}
function draw() {
var transform = d3.geoTransform({
point: projectPointCurry(leafletMap)
});
var geoPath = d3.geoPath()
.projection(transform);
d3.select(svgLayer._container)
.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();
});
},
function (update) {
return update;
},
function (exit) {
return exit.remove();
}
).attr('d', function (sat) {
return geoPath(clipMercator(sat.getFootprint()));
});
};
function animateSats(elapsed) {
var dateInMs = activeClock.elapsed(elapsed)
.date();
var date = new Date(dateInMs);
attributionControl.setPrefix(date);
updateSats(date);
draw();
window.requestAnimationFrame(animateSats);
}
init();
d3.text('tles.txt')
.then(parseTle)
.then(initSats);
}(window.L, window.d3, window.satellite))
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
@RozanMustafa
Copy link

hello sir, thank you for this great job
half-angle has a big impact on the footprint, what do you mean by it and what is difference between half-angle and elevation angle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment