|
(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)) |