const KM2PX = 1 / 20; const EARTH_RADIUS = 6371; // km const SAT_SIZE = 8; // px const INIT_HIDE_GROUPS = new Set(['Low Earth Orbit (MEO)']); const SatellitesView = Kapsule({ props: { satData: { default: [] }, onTimeChange: { default: () => {}} }, stateInit: () => ({ groupColor: d3.scaleOrdinal(d3.schemeCategory10), ObjRender: ThreeRenderObjects() .tooltipContent(o => o.hasOwnProperty('_data') && `${o._data.name} (${o._data.group})`), gui: new dat.GUI(), groupControls: {} }), init(domNode, state) { state.ObjRender(domNode) .cameraPosition({ y: 700, z: 4000 }); const timeStep = 10 * 1000; let time = new Date(); const ticker = new FrameTicker.default(); ticker.onTick.add(() => { time = new Date(+time + timeStep); state.onTimeChange(time); updateSatPositions(state.satData, time); updateEarthRotation(state.earth, time); state.ObjRender.tick(); }); state.groupsFolder = state.gui.addFolder('Types'); state.groupsFolder.open(); }, update(state) { const satGeometry = new THREE.SphereGeometry(SAT_SIZE / 2, 8, 8); state.satData.forEach(sat => { sat.threeObj = new THREE.Mesh(satGeometry, new THREE.MeshLambertMaterial({ color: state.groupColor(sat.group), transparent: true, opacity: 0.7 })); sat._show = !INIT_HIDE_GROUPS.has(sat.group); sat.threeObj._data = sat; // attach data }); const lights = [ new THREE.AmbientLight(0xbbbbbb), new THREE.DirectionalLight(0xffffff, 0.6) ]; state.earth = genEarth(EARTH_RADIUS * KM2PX); state.ObjRender.objects([].concat( ...lights, state.earth, ...state.satData.map(sat => sat.threeObj) )); // Gui options new Set(state.satData.map(sat => sat.group)).forEach(group => { if (state.groupControls.hasOwnProperty(group)) return; // Group option already exists state.groupControls[group] = !INIT_HIDE_GROUPS.has(group); state.groupsFolder.add(state.groupControls, group) .onChange(show => { state.satData.filter(sat => sat.group === group).forEach(sat => sat._show = show); state.ObjRender.objects([].concat( ...lights, state.earth, ...state.satData .filter(sat => sat._show) .map(sat => sat.threeObj) )); }); }); } }); function genCircle(radius, color) { const circleR = 16; const canvas = document.createElement('canvas'); canvas.width = circleR * 2; canvas.height = circleR * 2; const ctx = canvas.getContext('2d'); ctx.beginPath(); ctx.arc(circleR, circleR, circleR , 0, 2 * Math.PI); ctx.fillStyle = color; ctx.fill(); var texture = new THREE.Texture(canvas); texture.needsUpdate = true; // important! const material = new THREE.SpriteMaterial({ map: texture }); const sprite = new THREE.Sprite(material); sprite.scale.set(radius/2, radius/2); return sprite; } function genEarth(radius) { const earth = new THREE.Group(); // Terrain images from the blue marble: https://visibleearth.nasa.gov/ const earthElev = new THREE.TextureLoader().load('_earth-elevation-4k.png'); new THREE.TextureLoader().load('_earth-blue-marble-2k.jpg', earthTerrain => { earth.add(new THREE.Mesh( new THREE.SphereGeometry(radius, 32, 32), new THREE.MeshPhongMaterial({ map: earthTerrain, bumpMap: earthElev, bumpScale: 50 }) )); // Graticules const graticules = new THREE.Object3D(); drawThreeGeo( { geometry: d3.geoGraticule10(), type: 'Feature' }, radius, 'sphere', { color: 'lightgrey', transparent: true, opacity: 0.1 }, graticules ); earth.add(graticules); }); return earth; } function updateSatPositions(sats, posTime) { const coords = { x: 'z', y: 'x', z: 'y' }; sats .filter(sat => sat._show).forEach(sat => { const pos = satellite.propagate(sat.satrec, posTime); if (pos.position) { Object.entries(pos.position).forEach(([dimension, km]) => { //node[dimension] = km / 20; sat.threeObj.position[coords[dimension]] = km * KM2PX; }); } }); } function updateEarthRotation(earth, posTime) { if (earth) { const timeOfDayRatio = posTime.getUTCHours() / 24 + posTime.getUTCMinutes() / (24 * 60) + posTime.getUTCSeconds() / (24 * 60 * 60); earth.rotation.y = timeOfDayRatio * 2 * Math.PI; } }