globe sizing

This commit is contained in:
Daniel Vinci 2021-04-30 14:23:28 -06:00
parent ac3a890824
commit fb78deeed4
No known key found for this signature in database
GPG key ID: 7A700C29916C26CA
2 changed files with 394 additions and 392 deletions

View file

@ -1,335 +1,304 @@
<div class="globeContainer">
<svg id="globe" width="400" height="400"></svg>
<div class="globeText">
click and drag
</div>
</div>
<style>
.globeContainer {
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
background-color: #00111d;
color: #ffffff;
}
:global(.footprint--LEO) {
fill: rgba(255, 177, 64, 0.08);
stroke: rgba(255, 177, 64, 0.5);
}
.globeText{
text-align: center;
font-size: 0.6em;
color: #aaaaaa;
}
#globe {
display: block;
}
</style>
<script>
import * as d3 from "d3";
import * as sjs from 'satellite.js';
import { onMount } from 'svelte';
import tles from "../../static/tle.js"
import world110m from "../../static/world-110m.js"
import * as topojson from "topojson";
export let width = 300;
async function doThing() {
let satelliteJs = sjs;
var RADIANS = Math.PI / 180;
var DEGREES = 180 / Math.PI;
var R_EARTH = 6378.137; // equatorial radius (km)
import * as d3 from "d3";
import * as sjs from "satellite.js";
import { onMount } from "svelte";
import tles from "../../static/tle.js";
import world110m from "../../static/world-110m.js";
import * as topojson from "topojson";
/* =============================================== */
/* =============== CLOCK ========================= */
/* =============================================== */
export let width = 300;
/**
* Factory function for keeping track of elapsed time and rates.
*/
class Clock {
constructor() {
this._rate = 60; // 1ms elapsed : 60sec simulated
this._date = d3.now();
this._elapsed = 0;
async function doThing() {
let satelliteJs = sjs;
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.
*/
class Clock {
constructor() {
this._rate = 60; // 1ms elapsed : 60sec simulated
this._date = d3.now();
this._elapsed = 0;
}
async date(timeInMs) {
if (!arguments.length) return this._date + this._elapsed * this._rate;
this._date = timeInMs;
return this;
}
async elapsed(ms) {
if (!arguments.length) return this._date - d3.now(); // calculates elapsed
this._elapsed = ms;
return this;
}
async rate(secondsPerMsElapsed) {
if (!arguments.length) return this._rate;
this._rate = secondsPerMsElapsed;
return this;
}
}
async date(timeInMs) {
if (!arguments.length) return this._date + this._elapsed * this._rate;
this._date = timeInMs;
return this;
}
async elapsed(ms) {
if (!arguments.length) return this._date - d3.now(); // calculates elapsed
this._elapsed = ms;
return this;
}
async rate(secondsPerMsElapsed) {
if (!arguments.length) return this._rate;
this._rate = secondsPerMsElapsed;
return this;
}
}
/* ==================================================== */
/* =============== CONVERSION ========================= */
/* ==================================================== */
/* ==================================================== */
/* =============== CONVERSION ========================= */
/* ==================================================== */
async 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 ================================ */
/* ==================================================== */
async 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.
*/
class TLE {
constructor() {
this._properties;
/**
* Factory function for working with TLE.
*/
class TLE {
constructor() {
this._properties;
this._date;
}
async _lines(arry) {
return arry.slice(0, 2);
}
async satrecs(tles) {
return tles.map(function (d) {
return satelliteJs.twoline2satrec.apply(null, this._lines(d));
});
}
async features(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));
});
}
async lines(func) {
if (!arguments.length) return this._lines;
this._lines = func;
return this;
}
async properties(func) {
if (!arguments.length) return this._properties;
this._properties = func;
return this;
}
async date(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'], ...]
*/
async 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;
}
async _lines(arry) {
return arry.slice(0, 2);
}
async satrecs(tles) {
return tles.map(function (d) {
return satelliteJs.twoline2satrec.apply(null, this._lines(d));
});
}
async features(tles) {
var date = this._date || d3.now();
this._gmst;
return tles.map(function (d) {
var satrec = satelliteJs.twoline2satrec.apply(null, this._lines(d));
return satrecToFeature(satrec, date, this._properties(d));
});
this.setDate(date);
this.update();
this._orbitType = this.orbitTypeFromAlt(this._altitude); // LEO, MEO, or GEO
}
async lines(func) {
if (!arguments.length) return this._lines;
this._lines = func;
/**
* Updates satellite position and altitude based on current TLE and date
*/
Satellite.prototype.update = async 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;
}
async properties(func) {
if (!arguments.length) return this._properties;
this._properties = func;
return this;
}
async date(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'], ...]
*/
async 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
}
/**
* @returns {GeoJSON.Polygon} GeoJSON describing the satellite's current footprint on the Earth
*/
Satellite.prototype.getFootprint = function () {
var theta = this._halfAngle * RADIANS;
/**
* Updates satellite position and altitude based on current TLE and date
*/
Satellite.prototype.update = async function () {
var positionAndVelocity = satelliteJs.propagate(this._satrec, this._date);
var positionGd = satelliteJs.eciToGeodetic(
positionAndVelocity.position,
this._gmst
);
let coreAngle = this._coreAngle(theta, this._altitude, R_EARTH) * DEGREES;
this._position = {
lat: positionGd.latitude * DEGREES,
lng: positionGd.longitude * DEGREES,
return d3
.geoCircle()
.center([this._position.lng, this._position.lat])
.radius(coreAngle)();
};
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;
/**
* 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
);
};
let coreAngle = this._coreAngle(theta, this._altitude, R_EARTH) * DEGREES;
Satellite.prototype.halfAngle = function (halfAngle) {
if (!arguments.length) return this._halfAngle;
this._halfAngle = halfAngle;
return this;
};
return d3
.geoCircle()
.center([this._position.lng, this._position.lat])
.radius(coreAngle)();
};
Satellite.prototype.satNum = function (satNum) {
if (!arguments.length) return this._satNum;
this._satNum = satNum;
return this;
};
/**
* 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.altitude = function (altitude) {
if (!arguments.length) return this._altitude;
this._altitude = altitude;
return this;
};
Satellite.prototype.halfAngle = function (halfAngle) {
if (!arguments.length) return this._halfAngle;
this._halfAngle = halfAngle;
return this;
};
Satellite.prototype.position = function (position) {
if (!arguments.length) return this._position;
this._position = position;
return this;
};
Satellite.prototype.satNum = function (satNum) {
if (!arguments.length) return this._satNum;
this._satNum = satNum;
return this;
};
Satellite.prototype.getOrbitType = function () {
return this._orbitType;
};
Satellite.prototype.altitude = function (altitude) {
if (!arguments.length) return this._altitude;
this._altitude = altitude;
return this;
};
/**
* 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;
};
Satellite.prototype.position = function (position) {
if (!arguments.length) return this._position;
this._position = position;
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";
};
Satellite.prototype.getOrbitType = function () {
return this._orbitType;
};
/* =============================================== */
/* =================== GLOBE ===================== */
/* =============================================== */
/**
* 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;
};
// Approximate date the tle data was aquired from https://www.space-track.org/#recent
var TLE_DATA_DATE = new Date(2015, 11, 3, 17, 36).getTime();
/**
* 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";
};
var activeClock;
var sats;
/* =============================================== */
/* =================== GLOBE ===================== */
/* =============================================== */
var svg = d3.select("#globe");
// Approximate date the tle data was aquired from https://www.space-track.org/#recent
var TLE_DATA_DATE = new Date(2015, 11, 3, 17 ,36).getTime();
var marginTop = 0;
var width = svg.attr("width");
var height = svg.attr("height") - marginTop;
var activeClock;
var sats;
var projection = d3
.geoOrthographic()
.scale((height - 10) / 2)
.translate([width / 2, height / 2 + marginTop])
.rotate([45, -30]);
var svg = d3.select("#globe");
var geoPath = d3.geoPath().projection(projection);
var marginTop = 0;
var width = svg.attr("width");
var height = svg.attr("height") - marginTop;
svg
.append("path")
.datum({
type: "Sphere",
})
.style("cursor", "grab")
.attr("fill", "#2E86AB")
.attr("d", geoPath);
var projection = d3
.geoOrthographic()
.scale((height - 10) / 2)
.translate([width / 2, height / 2 + marginTop])
.rotate([45, -30]);
var geoPath = d3.geoPath().projection(projection);
svg
.append("path")
.datum({
type: "Sphere",
})
.style("cursor", "grab")
.attr("fill", "#2E86AB")
.attr("d", geoPath);
function initGlobe() {
let worldData = world110m;
function initGlobe() {
let worldData = world110m;
svg
.selectAll(".segment")
.data([topojson.feature(worldData, worldData.objects.land)])
@ -342,111 +311,144 @@
.style("stroke-width", "0px")
.style("fill", "#ffffff")
.style("opacity", "1");
}
}
async function updateSats(date) {
sats.forEach(async function (sat) {
return sat.setDate(date).update();
});
return sats;
}
async function updateSats(date) {
sats.forEach(async function (sat) {
return sat.setDate(date).update();
});
return sats;
}
/**
* Create satellite objects for each record in the TLEs and begin animation
* @param {string[][]} parsedTles
*/
async function initSats(parsedTles) {
activeClock = new Clock();
/**
* Create satellite objects for each record in the TLEs and begin animation
* @param {string[][]} parsedTles
*/
async function initSats(parsedTles) {
activeClock = new Clock();
await activeClock.rate(100);
await activeClock.date(TLE_DATA_DATE);
await activeClock.rate(100);
await activeClock.date(TLE_DATA_DATE);
sats = await Promise.all(
parsedTles.map(async function (tle) {
var sat = new Satellite(tle, new Date(2015, 11, 3, 17, 36));
sat.halfAngle(61.73);
return sat;
})
);
window.requestAnimationFrame(animateSats);
return sats;
}
async function draw() {
// redrawGlobe();
let allFootprints = svg.selectAll(".footprint");
let allFootprintsData = allFootprints.data(sats, async function (sat) {
return sat.satNum();
});
let allFootprintsDataJoin = allFootprintsData.join(function (enter) {
return enter
.append("path")
.attr("class", function (sat) {
return "footprint footprint--" + sat.getOrbitType();
sats = await Promise.all(
parsedTles.map(async function (tle) {
var sat = new Satellite(tle, new Date(2015, 11, 3, 17, 36));
sat.halfAngle(61.73);
return sat;
})
.style("cursor", "grab");
});
);
allFootprintsDataJoin.attr("d", function (sat) {
return geoPath(sat.getFootprint());
});
}
async function redrawGlobe() {
let allSelectedSegment = svg.selectAll(".segment");
allSelectedSegment.attr("d", geoPath);
}
var m0;
var o0;
async function mousedown(e) {
m0 = [e.pageX, e.pageY];
o0 = projection.rotate();
e.preventDefault();
}
async function mousemove(e) {
if (m0) {
var m1 = [e.pageX, e.pageY];
const o1 = [o0[0] + (m1[0] - m0[0]) / 2.5, o0[1] + (m0[1] - m1[1]) / 2.5];
projection.rotate(o1);
redrawGlobe();
window.requestAnimationFrame(animateSats);
return sats;
}
}
async function mouseup(e) {
if (m0) {
mousemove(e);
m0 = null;
async function draw() {
// redrawGlobe();
let allFootprints = svg.selectAll(".footprint");
let allFootprintsData = allFootprints.data(sats, async function (sat) {
return sat.satNum();
});
let allFootprintsDataJoin = allFootprintsData.join(function (enter) {
return enter
.append("path")
.attr("class", function (sat) {
return "footprint footprint--" + sat.getOrbitType();
})
.style("cursor", "grab");
});
allFootprintsDataJoin.attr("d", function (sat) {
return geoPath(sat.getFootprint());
});
}
async function redrawGlobe() {
let allSelectedSegment = svg.selectAll(".segment");
allSelectedSegment.attr("d", geoPath);
}
var m0;
var o0;
async function mousedown(e) {
m0 = [e.pageX, e.pageY];
o0 = projection.rotate();
e.preventDefault();
}
async function mousemove(e) {
if (m0) {
var m1 = [e.pageX, e.pageY];
const o1 = [
o0[0] + (m1[0] - m0[0]) / 2.5,
o0[1] + (m0[1] - m1[1]) / 2.5,
];
projection.rotate(o1);
redrawGlobe();
}
}
async function mouseup(e) {
if (m0) {
mousemove(e);
m0 = null;
}
}
svg.on("mousedown", mousedown);
d3.select(window).on("mousemove", mousemove).on("mouseup", mouseup);
async function animateSats(elapsed) {
var dateInMsI1 = await activeClock.elapsed(elapsed);
var dateInMs = dateInMsI1.date();
var date = new Date(await dateInMs);
updateSats(date);
draw();
window.requestAnimationFrame(animateSats);
}
initGlobe();
await initSats(await parseTle(tles));
}
svg.on("mousedown", mousedown);
d3.select(window).on("mousemove", mousemove).on("mouseup", mouseup);
onMount(async () => {
doThing();
});
</script>
async function animateSats(elapsed) {
var dateInMsI1 = await activeClock.elapsed(elapsed);
var dateInMs = dateInMsI1.date();
var date = new Date(await dateInMs);
<div class="globeContainer">
<svg id="globe" {width} height={width} />
<div class="globeText">click and drag</div>
</div>
updateSats(date);
draw();
window.requestAnimationFrame(animateSats);
<style>
.globeContainer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: #00111d;
color: #ffffff;
}
initGlobe();
await initSats(await parseTle(tles))
:global(.footprint--LEO) {
fill: rgba(255, 177, 64, 0.08);
stroke: rgba(255, 177, 64, 0.5);
}
onMount(async () => {
doThing();
});
</script>
.globeText {
text-align: center;
font-size: 0.6em;
color: #aaaaaa;
}
#globe {
display: block;
}
</style>

View file

@ -15,7 +15,7 @@
<div class="site">
<div class="hero">
<div class="container">
<Globe />
<Globe width="250" />
<FemtoHeader />
</div>
</div>
@ -105,7 +105,7 @@
.hero .container {
grid-template-columns: auto;
grid-template-rows: 200px auto;
grid-template-rows: 260px auto;
}
}
</style>