Skip to content

Instantly share code, notes, and snippets.

@bwswedberg
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 bwswedberg/d7d6eaedc2c501dc5f5e4be649b0da1d to your computer and use it in GitHub Desktop.
Save bwswedberg/d7d6eaedc2c501dc5f5e4be649b0da1d to your computer and use it in GitHub Desktop.
Satellite Map Using Leaflet + D3 with Wrapping

Another animated leaflet map but with d3.js and improved functionality. Satellites are added to leaflet SVG overlay pane. Each animation frame uses d3 to update the SVG overlay. If map pans or zooms beyond map bounds then it duplicates/wraps satellites.

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

Examples in Series:

Resources and Key Libs

Data

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

Inspirational Visualization

In-The-Sky.org

<!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://cdnjs.cloudflare.com/ajax/libs/satellite.js/3.0.0/satellite.min.js"></script>
<style>
body {
margin: 0;
}
#leaflet-map {
position: fixed;
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<div id="leaflet-map"></div>
<script src="index.js"></script>
</body>
</html>
(function(L, d3, satellite) {
/* =============================================== */
/* =============== CLOCK ========================= */
/* =============================================== */
/**
* Factory function for keeping track of elapsed time and rates.
*/
function clock() {
var rate = 60; // 1ms elapsed : 60sec simulated
var date = d3.now();
var elapsed = 0;
function clock() {}
clock.date = function(timeInMs) {
if (!arguments.length) return date + (elapsed * rate);
date = timeInMs;
return clock;
}
clock.elapsed = function(ms) {
if (!arguments.length) return date - d3.now(); // calculates elapsed
elapsed = ms;
return clock;
}
clock.rate = function(secondsPerMsElapsed) {
if (!arguments.length) return rate;
rate = secondsPerMsElapsed;
return clock;
}
return clock;
}
/* ==================================================== */
/* =============== CONVERSION ========================= */
/* ==================================================== */
function radiansToDegrees(radians) {
return radians * 180 / Math.PI;
}
function satrecToFeature(satrec, date, props) {
var properties = props || {};
var positionAndVelocity = satellite.propagate(satrec, date);
var gmst = satellite.gstime(date);
var positionGd = satellite.eciToGeodetic(positionAndVelocity.position, gmst);
properties.height = positionGd.height;
return {
type: 'Feature',
properties: properties,
geometry: {
type: 'Point',
coordinates: [
radiansToDegrees(positionGd.longitude),
radiansToDegrees(positionGd.latitude)
]
}
};
}
/**
* Given lngLat and longitude bounds this function interpolates the
* longitude so that you get appropriate amount of markers on map
* without weird wraping effects.
* @param {number[]} lngLat [lng, lat]
* @param {number} westBoundingLongitude
* @param {number} eastBoundingLongitude
* @return {number[][]} [[lng, lat], ...]
*/
function extrapolateWrappedCoordinates(lngLat, westBoundingLongitude, eastBoundingLongitude) {
var coords = [[lngLat[0], lngLat[1]]];
for (var lng0 = lngLat[0] - 360; westBoundingLongitude < lng0; lng0 -= 360) {
coords.push([lng0, lngLat[1]]);
}
for (var lng1 = lngLat[0] + 360; eastBoundingLongitude > lng1; lng1 += 360) {
coords.push([lng1, lngLat[1]]);
}
return coords;
}
/**
* Extrapolates a point geojson feature to fill west and east longitude
* bounds so that there is seamless wrap for maps over 180 or under -180 limits.
* @param {GeoJSONPointFeature} feature GeoJSON point feature
* @param {number} westBoundingLongitude
* @param {number} eastBoundingLongitude
* @return {GeoJSONFeatureCollection} Feature collection containing all points
*/
function extrapolateWrappedPointFeatures(feature, westBoundingLongitude, eastBoundingLongitude) {
const features = extrapolateWrappedCoordinates(feature.geometry.coordinates, westBoundingLongitude, eastBoundingLongitude)
.map(function(lngLat) {
return {
type: 'Feature',
properties: feature.properties,
geometry: {
type: 'Point',
coordinates: lngLat
}
};
})
return {
type: 'FeatureCollection',
features: features
};
}
/* ==================================================== */
/* =============== TLE ================================ */
/* ==================================================== */
/**
* Factory function for working with TLE.
*/
function tle() {
var _properties;
var _date;
var _lines = function (arry) {
return arry.slice(0, 2);
};
function tle() {}
tle.satrecs = function (tles) {
return tles.map(function(d) {
return satellite.twoline2satrec.apply(null, _lines(d));
});
}
tle.features = function (tles) {
var date = _date || d3.now();
return tles.map(function(d) {
var satrec = satellite.twoline2satrec.apply(null, _lines(d));
return satrecToFeature(satrec, date, _properties(d));
});
}
tle.lines = function (func) {
if (!arguments.length) return _lines;
_lines = func;
return tle;
}
tle.properties = function (func) {
if (!arguments.length) return _properties;
_properties = func;
return tle;
}
tle.date = function (ms) {
if (!arguments.length) return _date;
_date = ms;
return tle;
}
return tle;
}
/* ==================================================== */
/* =============== 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;
}, []);
}
/* =============================================== */
/* =============== 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 satrecs;
var svgLayer;
var path;
var heightColorScale = d3.scaleThreshold()
.domain([1200, 22000])
.range([d3.schemeCategory10[3], d3.schemeCategory10[2], d3.schemeCategory10[0]])
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'),
svgLayer
]
});
attributionControl = L.control.attribution({ prefix: ''})
.addTo(leafletMap);
var transform = d3.geoTransform({ point: projectPointCurry(leafletMap) });
path = d3.geoPath()
.projection(transform)
.pointRadius(2.5);
}
function update(parsedTles) {
satrecs = tle()
.date(TLE_DATA_DATE)
.satrecs(parsedTles);
activeClock = clock()
.rate(1000)
.date(TLE_DATA_DATE);
window.requestAnimationFrame(draw);
}
function draw(elapsed) {
var dateInMs = activeClock.elapsed(elapsed)
.date();
var date = new Date(dateInMs);
attributionControl.setPrefix(date);
var bounds = leafletMap.getBounds();
var westBoundingLongitude = bounds.getWest();
var eastBoundingLongitude = bounds.getEast();
var features = satrecs.reduce(function(acc, cur) {
var feature = satrecToFeature(cur, date);
var wrapped = extrapolateWrappedPointFeatures(feature, westBoundingLongitude, eastBoundingLongitude);
return acc.concat(wrapped.features);
}, []);
var feature = d3.select(svgLayer._container)
.selectAll('path')
.data(features);
feature.enter().append('path')
.merge(feature)
.attr('fill', function(d) {return heightColorScale(d.properties.height);})
.attr('stroke', function(d) {return heightColorScale(d.properties.height);})
.attr('d', path);
feature.exit().remove();
window.requestAnimationFrame(draw);
}
init();
d3.text('tles.txt')
.then(parseTle)
.then(update);
}(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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment