├── assets ├── borders.png ├── circle.png ├── earthmap.png ├── night2048.jpg ├── earthmap-high.jpg └── theme.css ├── screenshots ├── 01.png ├── 02.png ├── 03.png ├── 04.png ├── 05.png ├── 06.png ├── 07.png ├── 08.png ├── 09.png ├── 10.png └── 11.png ├── docs ├── circle.f4683951.png ├── earthmap-high.602450bd.jpg ├── index.html ├── satellite-tracker.6463a969.css └── satellite-tracker.6463a969.css.map ├── .babelrc ├── index.js ├── index.html ├── Search ├── SearchBox.js ├── Search.js └── SearchResults.js ├── Options ├── DateSlider.css └── DateSlider.js ├── fork.js ├── .gitignore ├── highlights.js ├── Info.js ├── .vscode └── launch.json ├── Selection └── SelectedStations.js ├── package.json ├── samples └── iss-1day │ └── index.js ├── LICENSE.md ├── tle.js ├── readme.md ├── App.js └── engine.js /assets/borders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/assets/borders.png -------------------------------------------------------------------------------- /assets/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/assets/circle.png -------------------------------------------------------------------------------- /screenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/01.png -------------------------------------------------------------------------------- /screenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/02.png -------------------------------------------------------------------------------- /screenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/03.png -------------------------------------------------------------------------------- /screenshots/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/04.png -------------------------------------------------------------------------------- /screenshots/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/05.png -------------------------------------------------------------------------------- /screenshots/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/06.png -------------------------------------------------------------------------------- /screenshots/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/07.png -------------------------------------------------------------------------------- /screenshots/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/08.png -------------------------------------------------------------------------------- /screenshots/09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/09.png -------------------------------------------------------------------------------- /screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/10.png -------------------------------------------------------------------------------- /screenshots/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/screenshots/11.png -------------------------------------------------------------------------------- /assets/earthmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/assets/earthmap.png -------------------------------------------------------------------------------- /assets/night2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/assets/night2048.jpg -------------------------------------------------------------------------------- /assets/earthmap-high.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/assets/earthmap-high.jpg -------------------------------------------------------------------------------- /docs/circle.f4683951.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/docs/circle.f4683951.png -------------------------------------------------------------------------------- /docs/earthmap-high.602450bd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuarezv/satellite-tracker/HEAD/docs/earthmap-high.602450bd.jpg -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | "@babel/preset-env" 5 | ], 6 | "plugins": [ 7 | [ 8 | "transform-class-properties" 9 | ] 10 | ] 11 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render } from "react-dom"; 3 | import App from "./App"; 4 | 5 | 6 | render(, document.getElementById("root")); 7 | 8 | if (module.hot) { 9 | module.hot.accept(); 10 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Satellite tracker 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Satellite tracker
-------------------------------------------------------------------------------- /Search/SearchBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({value, onChange}) => { 4 | return ( 5 | onChange && onChange(e.target.value)} 9 | placeholder='Search' 10 | /> 11 | ) 12 | } -------------------------------------------------------------------------------- /Options/DateSlider.css: -------------------------------------------------------------------------------- 1 | .DateSliderWrapper { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | width: 100%; 6 | padding: 20px; 7 | } 8 | 9 | .DateSlider { 10 | width: 100%; 11 | } 12 | 13 | .DateSliderWrapper .Value { 14 | display: block; 15 | margin-right: 40px; 16 | margin-bottom: 10px; 17 | } -------------------------------------------------------------------------------- /fork.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GitHubForkRibbon from 'react-github-fork-ribbon'; 3 | 4 | 5 | const Fork = () => { 6 | return ( 7 | 10 | Fork me on GitHub 11 | 12 | ) 13 | } 14 | 15 | export default Fork; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /buildprivate 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | stats.html 24 | 25 | 26 | .cache 27 | dist 28 | node_modules 29 | -------------------------------------------------------------------------------- /highlights.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Highlights = ({query, total}) => { 4 | if (!query) return null; 5 | 6 | return ( 7 |
8 |

Highlighting in orange satellites with name matching

9 |

{query}

10 |

{total} satellites

11 |
12 | ) 13 | } 14 | 15 | export default Highlights; -------------------------------------------------------------------------------- /Options/DateSlider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './DateSlider.css'; 3 | 4 | 5 | const DateSlider = ({min, max, value, onChange, onRender}) => { 6 | return ( 7 |
8 | {onRender ? {onRender(value)} : null} 9 | 10 |
11 | ) 12 | } 13 | 14 | export default DateSlider; -------------------------------------------------------------------------------- /Info.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Info extends Component { 4 | render() { 5 | const p = this.props; 6 | const { stations, refMode } = p; 7 | 8 | return ( 9 |
10 |

Satellite tracker

11 | {stations && stations.length > 0 && (

Total objects: {stations.length}

)} 12 | {refMode == 1 ?

ECF mode

:

ECI mode

} 13 |
14 | ) 15 | } 16 | } 17 | 18 | export default Info; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /Selection/SelectedStations.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StationCard } from '../Search/SearchResults'; 3 | 4 | export default function({selected, onRemoveStation, onRemoveAll, onStationClick}) { 5 | if (!selected || selected.length === 0) return null; 6 | 7 | return ( 8 |
9 |

Selected

10 |

Clear all

11 | {selected.map((station, i) => { 12 | return 18 | })} 19 |
20 | ) 21 | } -------------------------------------------------------------------------------- /Search/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SearchResults from './SearchResults'; 3 | import SearchBox from './SearchBox'; 4 | 5 | class Search extends Component { 6 | 7 | state = { 8 | searchText: '' 9 | } 10 | 11 | handleSearchChanged = (val) => { 12 | this.setState({searchText: val}); 13 | } 14 | 15 | render() { 16 | const { stations, onResultClick } = this.props; 17 | 18 | return ( 19 |
20 | 21 | 22 |
23 | ) 24 | } 25 | } 26 | 27 | export default Search; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "satellite-tracker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html", 9 | "publish": "parcel build index.html --public-url ./ --out-dir docs" 10 | }, 11 | "author": "David Suárez (github.com/dsuarezv)", 12 | "dependencies": { 13 | "assets": "^3.0.1", 14 | "query-string": "6.10.1", 15 | "react": "^16.14.0", 16 | "react-dom": "^16.14.0", 17 | "react-github-fork-ribbon": "0.6.0", 18 | "react-router": "5.1.2", 19 | "satellite.js": "^4.1.3", 20 | "three": "^0.138.3" 21 | }, 22 | "devDependencies": { 23 | "@babel/cli": "^7.12.16", 24 | "@babel/core": "^7.12.16", 25 | "@babel/plugin-proposal-class-properties": "^7.12.13", 26 | "@babel/preset-react": "^7.12.13", 27 | "@types/react": "^16.14.4", 28 | "babel-plugin-transform-class-properties": "^6.24.1", 29 | "parcel-bundler": "^1.12.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/iss-1day/index.js: -------------------------------------------------------------------------------- 1 | import { Engine } from '../../lib' 2 | 3 | // Bypass CORS 4 | function getCorsFreeUrl(url) { 5 | return 'https://cors-anywhere.herokuapp.com/' + url; 6 | } 7 | 8 | 9 | var target = document.getElementById("globe"); 10 | var engine = new Engine(); 11 | engine.initialize(target, { 12 | backgroundColor: 0xEEEEEE 13 | }); 14 | 15 | engine.loadLteFileStations(getCorsFreeUrl('http://www.celestrak.com/NORAD/elements/stations.txt'), 0xFFFFFF, null, s => { 16 | // Return true for the station to be displayed. False otherwise. 17 | // Also, properties of the station can be altered here (like orbitMinutes to display the orbit). 18 | if (s.name === 'ISS (ZARYA)') { 19 | s.orbitMinutes = 24 * 60; 20 | return true; 21 | } 22 | 23 | return false; 24 | }); 25 | 26 | function update() { 27 | engine.updateAllPositions(new Date()) 28 | } 29 | 30 | setInterval(update, 1000); 31 | 32 | 33 | // Parcel hot reloading 34 | if (module.hot) { 35 | module.hot.accept(); 36 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Suarez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Search/SearchResults.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MaxSearchResults = 100; 4 | 5 | const filterResults = (stations, searchText) => { 6 | if (!stations) return null; 7 | if (!searchText || searchText === '') return null; 8 | 9 | const regex = new RegExp(searchText, 'i'); 10 | 11 | return stations.filter(station => regex.test(station.name)).slice(0, MaxSearchResults); 12 | } 13 | 14 | 15 | const SearchResults = ({stations, searchText, onResultClick}) => { 16 | const results = filterResults(stations, searchText); 17 | if (!results) return null; 18 | 19 | return ( 20 |
21 | {results.map((result, i) => )} 22 |
23 | ) 24 | } 25 | 26 | 27 | export const StationCard = ({station, onClick, onRemoveClick, className}) => { 28 | 29 | const noradId = station.satrec && station.satrec.satnum; 30 | 31 | return ( 32 |
onClick && onClick(station)}> 33 |

34 | {station.name} 35 | {onRemoveClick && onRemoveClick(station)}>x} 36 |

37 |
38 | ) 39 | } 40 | 41 | 42 | export default SearchResults; -------------------------------------------------------------------------------- /docs/satellite-tracker.6463a969.css: -------------------------------------------------------------------------------- 1 | body,div,h1,h2,li,p,span,ul{margin:0;padding:0;box-sizing:border-box}body{background-color:#041119;color:#fff;font-family:Montserrat,sans-serif;font-size:12px}h1{text-transform:uppercase}.SearchBox{border-radius:0;padding:5px 0;width:100%;border:none;border-bottom:2px solid #fff;background-color:transparent;color:#fff;font-size:2em;outline:none}.Result{margin:3px 6px 3px 0;border:1px solid rgba(102,113,136,.39);border-left:3px solid #ffbfa3;background-color:rgba(48,67,104,.5);border-top-right-radius:10px;padding:4px 10px;width:150px;cursor:pointer;overflow:hidden;white-space:nowrap}.RemoveButton{float:right;cursor:pointer}.SmallButton{cursor:pointer;font-size:.8em}.Highlights{text-align:center}@media only screen and (max-width:999px){.Info{top:10px}.Info,.Search{position:absolute;left:10px}.Search{top:60px;width:120px}.Selected{position:absolute;right:10px;top:40px;width:120px}.Highlights{position:absolute;bottom:10px;width:100%}.Hint{font-size:.9em}.Highlight{font-size:3em}.ResultsWrapper{max-height:80vh;overflow:hidden;width:150px}}@media screen and (min-width:1000px){.Info{position:absolute;top:100px;left:50px}.Search{left:50px}.Search,.Selected{position:absolute;top:180px;width:200px}.Selected{right:50px}.Highlights{position:absolute;top:10px;width:100%}.Hint{font-size:.9em}.Highlight{font-size:3em}.ResultsWrapper{margin-top:10px;display:flex;flex-direction:column;flex-wrap:wrap;justify-content:flex-start;align-items:flex-start;height:60vh;max-width:70vw}}.DateSliderWrapper{position:fixed;bottom:0;left:0;width:100%;padding:20px}.DateSlider{width:100%}.DateSliderWrapper .Value{display:block;margin-right:40px;margin-bottom:10px} 2 | /*# sourceMappingURL=satellite-tracker.6463a969.css.map */ -------------------------------------------------------------------------------- /tle.js: -------------------------------------------------------------------------------- 1 | import * as satellite from 'satellite.js/lib/index'; 2 | 3 | 4 | export const EarthRadius = 6371; 5 | 6 | const rad2Deg = 180 / 3.141592654; 7 | 8 | export const parseTleFile = (fileContent, stationOptions) => { 9 | const result = []; 10 | const lines = fileContent.split("\n"); 11 | let current = null; 12 | 13 | for (let i = 0; i < lines.length; ++i) { 14 | const line = lines[i].trim(); 15 | 16 | if (line.length === 0) continue; 17 | 18 | if (line[0] === '1') { 19 | current.tle1 = line; 20 | } 21 | else if (line[0] === '2') { 22 | current.tle2 = line; 23 | } 24 | else { 25 | current = { 26 | name: line, 27 | ...stationOptions 28 | }; 29 | result.push(current); 30 | } 31 | } 32 | 33 | return result; 34 | } 35 | 36 | 37 | // __ Satellite locations _________________________________________________ 38 | 39 | 40 | const latLon2Xyz = (radius, lat, lon) => { 41 | var phi = (90-lat)*(Math.PI/180) 42 | var theta = (lon+180)*(Math.PI/180) 43 | 44 | const x = -((radius) * Math.sin(phi) * Math.cos(theta)) 45 | const z = ((radius) * Math.sin(phi) * Math.sin(theta)) 46 | const y = ((radius) * Math.cos(phi)) 47 | 48 | return { x, y, z }; 49 | } 50 | 51 | const toThree = (v) => { 52 | return { x: v.x, y: v.z, z: -v.y }; 53 | } 54 | 55 | const getSolution = (station, date) => { 56 | 57 | if (!station.satrec) { 58 | const { tle1, tle2 } = station; 59 | if (!tle1 || !tle2) return null; 60 | station.satrec = satellite.twoline2satrec(tle1, tle2);; 61 | } 62 | 63 | return satellite.propagate(station.satrec, date); 64 | } 65 | 66 | 67 | // type: 1 ECEF coordinates 2: ECI coordinates 68 | export const getPositionFromTle = (station, date, type = 1) => { 69 | if (!station || !date) return null; 70 | 71 | const positionVelocity = getSolution(station, date); 72 | 73 | const positionEci = positionVelocity.position; 74 | if (!positionEci) return null; // Ignore 75 | 76 | if (type === 2) return toThree(positionEci); 77 | 78 | const gmst = satellite.gstime(date); 79 | const positionEcf = satellite.eciToEcf(positionEci, gmst); 80 | return toThree(positionEcf); 81 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Satellite tracker 2 | ================= 3 | 4 | Javascript 3D satellite tracker with up-to-date data from CELESTRAK. Uses [Three.js](https://threejs.org/), [React](https://reactjs.org/) and [satellite.js](https://github.com/shashwatak/satellite-js) for orbit prediction. 5 | 6 | It is meant as a simple 3D engine that can be used to draw your own satellite orbits. Out of the box it can filter and highlight satellites by name or clicking directly on them, but you can extend / reuse it by looking at the App.js file and loading the satellite set you prefer or changing how orbits are displayed. 7 | 8 | [Live DEMO displaying IIS orbit](https://dsuarezv.github.io/satellite-tracker?ss=25544) 9 | 10 | [Live DEMO highlighting SpaceX's StarLink constellation](https://dsuarezv.github.io/satellite-tracker?highlight=starlink) 11 | 12 | Here is a nice screenshot showing the predicted International Space Station orbit through a day. (Side note, [Why doesn't ISS pass over the polar regions?](https://space.stackexchange.com/questions/5297/why-doesnt-iss-pass-over-the-polar-regions)). The orbits are displayed in the ECEF (Earth Centered Earth Fixed) reference frame. 13 | 14 | ![International Space Station](screenshots/01.png) 15 | 16 | StarLink satellites highlighted in orange, some of them displaying orbits: 17 | 18 | ![](screenshots/11.png) 19 | 20 | Some random satellites selected: 21 | 22 | ![](screenshots/07.png) 23 | 24 | Some COSMOS satellites with orbits. Search and select interface: 25 | 26 | ![](screenshots/05.png) 27 | 28 | Beidou satellites orbits in ECI reference frame. ECI mode can be enabled by setting UseDateSlider = true in App.js. Please note that this mode needs further testing and is not complete yet. 29 | 30 | ![](screenshots/10.png) 31 | 32 | 33 | Active objects from CELESTRAK (http://www.celestrak.com/NORAD/elements/active.txt) 34 | 35 | ![Active satellites](screenshots/02.png) 36 | 37 | Here debris from cosmos-2251 in red, active sats in blue, stations in yellow: 38 | 39 | ![debris](screenshots/04.png) 40 | 41 | Installation 42 | ============ 43 | 44 | $ git clone https://github.com/dsuarezv/satellite-tracker 45 | $ cd satellite-tracker 46 | $ npm install 47 | $ npm start 48 | 49 | That should start a [parcel](https://parceljs.org/) dev server. Browse to http://localhost:1234 to see it in action. In case parcel is not installed, follow instructions on their site. You should be able to run it with this command: 50 | 51 | $ parcel index.html 52 | -------------------------------------------------------------------------------- /assets/theme.css: -------------------------------------------------------------------------------- 1 | body, h1, h2, p, div, span, li, ul { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | background-color: #041119; 9 | color: white; 10 | font-family: Montserrat, sans-serif; 11 | font-size: 12px; 12 | } 13 | 14 | h1 { 15 | text-transform: uppercase; 16 | } 17 | 18 | 19 | .SearchBox { 20 | border: none; 21 | border-radius: 0px; 22 | padding: 5px 0; 23 | width: 100%; 24 | border-bottom: 2px solid white; 25 | background-color: transparent; 26 | color: white; 27 | font-size: 2em; 28 | outline: none; 29 | } 30 | 31 | .Result { 32 | margin: 3px 6px 3px 0; 33 | border: 1px solid rgba(102, 113, 136, 0.39); 34 | border-left: 3px solid #ffbfa3; 35 | background-color: rgba(48, 67, 104, 0.5); 36 | border-top-right-radius: 10px; 37 | padding: 4px 10px; 38 | width: 150px; 39 | 40 | cursor: pointer; 41 | overflow: hidden; 42 | white-space: nowrap; 43 | } 44 | 45 | 46 | .RemoveButton { 47 | float: right; 48 | cursor: pointer; 49 | } 50 | 51 | .SmallButton { 52 | cursor: pointer; 53 | font-size: 0.8em; 54 | } 55 | 56 | .Highlights { 57 | text-align: center; 58 | } 59 | 60 | 61 | @media only screen and (max-width: 999px) { 62 | .Info { 63 | position: absolute; 64 | top: 10px; 65 | left: 10px; 66 | } 67 | 68 | .Search { 69 | position: absolute; 70 | top: 60px; 71 | left: 10px; 72 | width: 120px; 73 | } 74 | 75 | .Selected { 76 | position: absolute; 77 | right: 10px; 78 | top: 40px; 79 | width: 120px; 80 | } 81 | 82 | .Highlights { 83 | position: absolute; 84 | bottom: 10px; 85 | width: 100%; 86 | } 87 | 88 | .Hint { font-size: 0.9em;} 89 | .Highlight { font-size: 3em;} 90 | 91 | .ResultsWrapper { 92 | max-height: 80vh; 93 | overflow: hidden; 94 | width: 150px; 95 | } 96 | } 97 | 98 | @media screen and (min-width: 1000px) { 99 | 100 | .Info { 101 | position: absolute; 102 | top: 100px; 103 | left: 50px; 104 | } 105 | 106 | .Search { 107 | position: absolute; 108 | top: 180px; 109 | left: 50px; 110 | width: 200px; 111 | } 112 | 113 | .Selected { 114 | position: absolute; 115 | right: 50px; 116 | top: 180px; 117 | width: 200px; 118 | } 119 | 120 | .Highlights { 121 | position: absolute; 122 | top: 10px; 123 | width: 100%; 124 | } 125 | 126 | .Hint { font-size: 0.9em;} 127 | .Highlight { font-size: 3em;} 128 | 129 | .ResultsWrapper { 130 | margin-top: 10px; 131 | display: flex; 132 | flex-direction: column; 133 | flex-wrap: wrap; 134 | justify-content: flex-start; 135 | align-items: flex-start; 136 | height: 60vh; 137 | max-width: 70vw; 138 | } 139 | } -------------------------------------------------------------------------------- /docs/satellite-tracker.6463a969.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["theme.css","DateSlider.css"],"names":[],"mappings":"AAAA,4BACI,QAAS,CACT,SAAU,CACV,qBACJ,CAEA,KACI,wBAAyB,CACzB,UAAY,CACZ,iCAAmC,CACnC,cACJ,CAEA,GACI,wBACJ,CAGA,WAEI,eAAkB,CAClB,aAAc,CACd,UAAW,CACX,WAA8B,CAA9B,4BAA8B,CAC9B,4BAA6B,CAC7B,UAAY,CACZ,aAAc,CACd,YACJ,CAEA,QACI,oBAAqB,CAErB,sCAA8B,CAA9B,6BAA8B,CAC9B,mCAAwC,CACxC,4BAA6B,CAC7B,gBAAiB,CACjB,WAAY,CAEZ,cAAe,CACf,eAAgB,CAChB,kBACJ,CAGA,cACI,WAAY,CACZ,cACJ,CAEA,aACI,cAAe,CACf,cACJ,CAEA,YACI,iBACJ,CAGA,yCACI,MAEI,QAEJ,CAEA,cALI,iBAAkB,CAElB,SAQJ,CALA,QAEI,QAAS,CAET,WACJ,CAEA,UACI,iBAAkB,CAClB,UAAW,CACX,QAAS,CACT,WACJ,CAEA,YACI,iBAAkB,CAClB,WAAY,CACZ,UACJ,CAEA,MAAQ,cAAiB,CACzB,WAAa,aAAe,CAE5B,gBACI,eAAgB,CAChB,eAAgB,CAChB,WACJ,CACJ,CAEA,qCAEI,MACI,iBAAkB,CAClB,SAAU,CACV,SACJ,CAEA,QAGI,SAEJ,CAEA,kBANI,iBAAkB,CAClB,SAAU,CAEV,WAQJ,CALA,UAEI,UAGJ,CAEA,YACI,iBAAkB,CAClB,QAAS,CACT,UACJ,CAEA,MAAQ,cAAiB,CACzB,WAAa,aAAe,CAE5B,gBACI,eAAgB,CAChB,YAAa,CACb,qBAAsB,CACtB,cAAe,CACf,0BAA2B,CAC3B,sBAAuB,CACvB,WAAY,CACZ,cACJ,CACJ,CC1IA,mBACI,cAAe,CACf,QAAS,CACT,MAAO,CACP,UAAW,CACX,YACJ,CAEA,YACI,UACJ,CAEA,0BACI,aAAc,CACd,iBAAkB,CAClB,kBACJ","file":"satellite-tracker.6463a969.css","sourceRoot":"..","sourcesContent":["body, h1, h2, p, div, span, li, ul {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\nbody {\n background-color: #041119;\n color: white;\n font-family: Montserrat, sans-serif;\n font-size: 12px;\n}\n\nh1 {\n text-transform: uppercase;\n}\n\n\n.SearchBox {\n border: none;\n border-radius: 0px;\n padding: 5px 0;\n width: 100%;\n border-bottom: 2px solid white;\n background-color: transparent;\n color: white;\n font-size: 2em;\n outline: none;\n}\n\n.Result {\n margin: 3px 6px 3px 0;\n border: 1px solid rgba(102, 113, 136, 0.39);\n border-left: 3px solid #ffbfa3;\n background-color: rgba(48, 67, 104, 0.5);\n border-top-right-radius: 10px;\n padding: 4px 10px;\n width: 150px;\n \n cursor: pointer;\n overflow: hidden;\n white-space: nowrap;\n}\n\n\n.RemoveButton {\n float: right;\n cursor: pointer;\n}\n\n.SmallButton {\n cursor: pointer;\n font-size: 0.8em;\n}\n\n.Highlights {\n text-align: center;\n}\n\n\n@media only screen and (max-width: 999px) {\n .Info {\n position: absolute;\n top: 10px;\n left: 10px;\n }\n \n .Search {\n position: absolute;\n top: 60px;\n left: 10px;\n width: 120px;\n }\n \n .Selected {\n position: absolute;\n right: 10px;\n top: 40px;\n width: 120px;\n }\n\n .Highlights {\n position: absolute;\n bottom: 10px;\n width: 100%;\n }\n\n .Hint { font-size: 0.9em;}\n .Highlight { font-size: 3em;}\n\n .ResultsWrapper {\n max-height: 80vh;\n overflow: hidden;\n width: 150px;\n }\n}\n\n@media screen and (min-width: 1000px) {\n\n .Info {\n position: absolute;\n top: 100px;\n left: 50px;\n }\n \n .Search {\n position: absolute;\n top: 180px;\n left: 50px;\n width: 200px;\n }\n \n .Selected {\n position: absolute;\n right: 50px;\n top: 180px;\n width: 200px;\n }\n \n .Highlights {\n position: absolute;\n top: 10px;\n width: 100%;\n }\n\n .Hint { font-size: 0.9em;}\n .Highlight { font-size: 3em;}\n \n .ResultsWrapper {\n margin-top: 10px;\n display: flex;\n flex-direction: column;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: flex-start;\n height: 60vh;\n max-width: 70vw;\n }\n}",".DateSliderWrapper {\r\n position: fixed;\r\n bottom: 0;\r\n left: 0;\r\n width: 100%;\r\n padding: 20px;\r\n}\r\n\r\n.DateSlider {\r\n width: 100%;\r\n}\r\n\r\n.DateSliderWrapper .Value {\r\n display: block;\r\n margin-right: 40px;\r\n margin-bottom: 10px;\r\n}"]} -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import "./assets/theme.css"; 3 | import { Engine } from './engine'; 4 | import Info from './Info'; 5 | import Search from './Search/Search'; 6 | import SelectedStations from './Selection/SelectedStations'; 7 | import Fork from './fork'; 8 | import * as qs from 'query-string'; 9 | import Highlights from './highlights'; 10 | import DateSlider from './Options/DateSlider'; 11 | 12 | // Some config 13 | const UseDateSlider = false; 14 | const DateSliderRangeInMilliseconds = 24 * 60 * 60 * 1000; // 24 hours 15 | 16 | // Bypass CORS 17 | function getCorsFreeUrl(url) { 18 | return url; 19 | } 20 | 21 | class App extends Component { 22 | 23 | state = { 24 | selected: [], 25 | stations: [], 26 | query: null, 27 | queryObjectCount: 0, 28 | initialDate: new Date().getTime(), 29 | currentDate: new Date().getTime(), 30 | referenceFrame: UseDateSlider ? 2 : 1 31 | } 32 | 33 | componentDidMount() { 34 | this.engine = new Engine(); 35 | this.engine.referenceFrame = this.state.referenceFrame; 36 | this.engine.initialize(this.el, { 37 | onStationClicked: this.handleStationClicked 38 | }); 39 | this.addStations(); 40 | 41 | this.engine.updateAllPositions(new Date()) 42 | 43 | setInterval(this.handleTimer, 1000); 44 | } 45 | 46 | componentWillUnmount() { 47 | this.engine.dispose(); 48 | } 49 | 50 | processQuery = (stations) => { 51 | const q = window.location.search; 52 | if (!q) return; 53 | 54 | const params = qs.parse(q); 55 | 56 | if (params.ss) { 57 | const selectedIds = params.ss.split(','); 58 | if (!selectedIds || selectedIds.length === 0) return; 59 | 60 | selectedIds.forEach(id => { 61 | const station = this.findStationById(stations, id); 62 | if (station) this.selectStation(station); 63 | }); 64 | } 65 | 66 | if (params.highlight) { 67 | const query = params.highlight; 68 | const matches = this.queryStationsByName(stations, query); 69 | matches.forEach(st => this.engine.highlightStation(st)); 70 | this.setState({...this.state, query, queryObjectCount: matches.length }); 71 | } 72 | } 73 | 74 | queryStationsByName = (stations, query) => { 75 | query = query.toLowerCase(); 76 | return stations.filter(st => st.name.toLowerCase().indexOf(query) > -1) 77 | } 78 | 79 | findStationById = (stations, id) => { 80 | return stations.find(st => st.satrec && st.satrec.satnum == id); 81 | } 82 | 83 | handleStationClicked = (station) => { 84 | if (!station) return; 85 | 86 | this.toggleSelection(station); 87 | } 88 | 89 | toggleSelection(station) { 90 | if (this.isSelected(station)) 91 | this.deselectStation(station); 92 | else 93 | this.selectStation(station); 94 | } 95 | 96 | isSelected = (station) => { 97 | return this.state.selected.includes(station); 98 | } 99 | 100 | selectStation = (station) => { 101 | const newSelected = this.state.selected.concat(station); 102 | this.setState({selected: newSelected}); 103 | 104 | this.engine.addOrbit(station); 105 | } 106 | 107 | deselectStation = (station) => { 108 | const newSelected = this.state.selected.filter(s => s !== station); 109 | this.setState( { selected: newSelected } ); 110 | 111 | this.engine.removeOrbit(station); 112 | } 113 | 114 | addStations = () => { 115 | this.addCelestrakSets(); 116 | //this.engine.addSatellite(ISS); 117 | //this.addAmsatSets(); 118 | } 119 | 120 | addCelestrakSets = () => { 121 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/weather.txt'), 0x00ffff) 122 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/cosmos-2251-debris.txt'), 0xff0090) 123 | this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=tle'), 0xffffff) 124 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/science.txt'), 0xffff00) 125 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/stations.txt'), 0xffff00) 126 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/iridium-NEXT.txt'), 0x00ff00) 127 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/gps-ops.txt'), 0x00ff00) 128 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/ses.txt'), 0xffffff) 129 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/starlink.txt'), 0x0000ff) 130 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/gps-ops.txt'), 0xffffff, { orbitMinutes: 0, satelliteSize: 200 }) 131 | //this.engine.loadLteFileStations(getCorsFreeUrl('https://celestrak.org/NORAD/elements/glo-ops.txt'), 0xff0000, { orbitMinutes: 500, satelliteSize: 500 }) 132 | .then(stations => { 133 | this.setState({stations}); 134 | this.processQuery(stations); 135 | }); 136 | 137 | } 138 | 139 | addAmsatSets = () => { 140 | this.engine.loadLteFileStations(getCorsFreeUrl('https://www.amsat.org/tle/current/nasabare.txt'), 0xffff00); 141 | } 142 | 143 | handleTimer = () => { 144 | // By default, update in realtime every second, unless dateSlider is displayed. 145 | if (!UseDateSlider) this.handleDateChange(null, new Date()); 146 | } 147 | 148 | handleSearchResultClick = (station) => { 149 | if (!station) return; 150 | 151 | this.toggleSelection(station); 152 | } 153 | 154 | handleRemoveSelected = (station) => { 155 | if (!station) return; 156 | 157 | this.deselectStation(station); 158 | } 159 | 160 | handleRemoveAllSelected = () => { 161 | this.state.selected.forEach(s => this.engine.removeOrbit(s)); 162 | this.setState({selected: []}); 163 | } 164 | 165 | handleReferenceFrameChange = () => { 166 | this.state.selected.forEach(s => this.engine.removeOrbit(s)); 167 | 168 | const newType = this.state.referenceFrame === 1 ? 2 : 1; 169 | this.setState({referenceFrame: newType}); 170 | this.engine.setReferenceFrame(newType); 171 | 172 | this.state.selected.forEach(s => this.engine.addOrbit(s)); 173 | } 174 | 175 | handleDateChange = (v, d) => { 176 | const newDate = v ? v.target.value : d; 177 | this.setState({ currentDate: newDate }); 178 | 179 | const date = new Date(); 180 | date.setTime(newDate); 181 | this.engine.updateAllPositions(date); 182 | } 183 | 184 | renderDate = (v) => { 185 | const result = new Date(); 186 | result.setTime(v); 187 | return result.toString(); 188 | } 189 | 190 | render() { 191 | const { selected, stations, initialDate, currentDate } = this.state; 192 | 193 | const maxMs = initialDate + DateSliderRangeInMilliseconds; 194 | 195 | return ( 196 |
197 | 198 | 199 | 200 | 201 | 202 | {UseDateSlider && } 203 |
this.el = c} style={{ width: '99%', height: '99%' }} /> 204 |
205 | ) 206 | } 207 | } 208 | 209 | 210 | 211 | export default App; -------------------------------------------------------------------------------- /engine.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; 3 | import earthmap from './assets/earthmap-high.jpg'; 4 | import circle from './assets/circle.png'; 5 | import { parseTleFile as parseTleFile, getPositionFromTle } from "./tle"; 6 | import { earthRadius } from "satellite.js/lib/constants"; 7 | import * as satellite from 'satellite.js/lib/index'; 8 | 9 | const SatelliteSize = 50; 10 | const MinutesPerDay = 1440; 11 | const ixpdotp = MinutesPerDay / (2.0 * 3.141592654) ; 12 | 13 | let TargetDate = new Date(); 14 | 15 | const defaultOptions = { 16 | backgroundColor: 0x041119, 17 | defaultSatelliteColor: 0xff0000, 18 | onStationClicked: null 19 | } 20 | 21 | const defaultStationOptions = { 22 | orbitMinutes: 0, 23 | satelliteSize: 50 24 | } 25 | 26 | export class Engine { 27 | 28 | stations = []; 29 | referenceFrame = 1; 30 | 31 | initialize(container, options = {}) { 32 | this.el = container; 33 | this.raycaster = new THREE.Raycaster(); 34 | this.options = { ...defaultOptions, ...options }; 35 | 36 | this._setupScene(); 37 | this._setupLights(); 38 | this._addBaseObjects(); 39 | 40 | this.render(); 41 | 42 | window.addEventListener('resize', this.handleWindowResize); 43 | window.addEventListener('pointerdown', this.handleMouseDown); 44 | } 45 | 46 | dispose() { 47 | window.removeEventListener('pointerdown', this.handleMouseDown); 48 | window.removeEventListener('resize', this.handleWindowResize); 49 | //window.cancelAnimationFrame(this.requestID); 50 | 51 | this.raycaster = null; 52 | this.el = null; 53 | 54 | this.controls.dispose(); 55 | } 56 | 57 | handleWindowResize = () => { 58 | const width = this.el.clientWidth; 59 | const height = this.el.clientHeight; 60 | 61 | this.renderer.setSize(width, height); 62 | this.camera.aspect = width / height; 63 | this.camera.updateProjectionMatrix(); 64 | 65 | this.render(); 66 | }; 67 | 68 | handleMouseDown = (e) => { 69 | const mouse = new THREE.Vector2( 70 | (e.clientX / this.el.clientWidth ) * 2 - 1, 71 | -(e.clientY / this.el.clientHeight ) * 2 + 1 ); 72 | 73 | this.raycaster.setFromCamera(mouse, this.camera); 74 | 75 | let station = null; 76 | 77 | var intersects = this.raycaster.intersectObjects(this.scene.children, true); 78 | if (intersects && intersects.length > 0) { 79 | const picked = intersects[0].object; 80 | if (picked) { 81 | station = this._findStationFromMesh(picked); 82 | } 83 | } 84 | 85 | const cb = this.options.onStationClicked; 86 | if (cb) cb(station); 87 | } 88 | 89 | 90 | // __ API _________________________________________________________________ 91 | 92 | 93 | addSatellite = (station, color, size) => { 94 | 95 | //const sat = this._getSatelliteMesh(color, size); 96 | const sat = this._getSatelliteSprite(color, size); 97 | const pos = this._getSatellitePositionFromTle(station); 98 | if (!pos) return; 99 | //const pos = { x: Math.random() * 20000 - 10000, y: Math.random() * 20000 - 10000 , z: Math.random() * 20000 - 10000, } 100 | 101 | sat.position.set(pos.x, pos.y, pos.z); 102 | station.mesh = sat; 103 | 104 | this.stations.push(station); 105 | 106 | if (station.orbitMinutes > 0) this.addOrbit(station); 107 | 108 | this.earth.add(sat); 109 | } 110 | 111 | loadLteFileStations = (url, color, stationOptions) => { 112 | const options = { ...defaultStationOptions, ...stationOptions }; 113 | 114 | return fetch(url).then(res => { 115 | if (res.ok) { 116 | return res.text().then(text => { 117 | return this._addTleFileStations(text, color, options); 118 | 119 | }); 120 | } 121 | }); 122 | } 123 | 124 | addOrbit = (station) => { 125 | if (station.orbitMinutes > 0) return; 126 | 127 | const revsPerDay = station.satrec.no * ixpdotp; 128 | const intervalMinutes = 1; 129 | const minutes = station.orbitMinutes || MinutesPerDay / revsPerDay; 130 | const initialDate = new Date(); 131 | 132 | //console.log('revsPerDay', revsPerDay, 'minutes', minutes); 133 | 134 | if (!this.orbitMaterial) { 135 | this.orbitMaterial = new THREE.LineBasicMaterial({color: 0x999999, opacity: 1.0, transparent: true }); 136 | } 137 | 138 | var points = []; 139 | 140 | for (var i = 0; i <= minutes; i += intervalMinutes) { 141 | const date = new Date(initialDate.getTime() + i * 60000); 142 | 143 | const pos = getPositionFromTle(station, date, this.referenceFrame); 144 | if (!pos) continue; 145 | 146 | points.push(new THREE.Vector3(pos.x, pos.y, pos.z)); 147 | } 148 | 149 | const geometry = new THREE.BufferGeometry().setFromPoints(points); 150 | var orbitCurve = new THREE.Line(geometry, this.orbitMaterial); 151 | station.orbit = orbitCurve; 152 | station.mesh.material = this.selectedMaterial; 153 | 154 | this.earth.add(orbitCurve); 155 | this.render(); 156 | } 157 | 158 | removeOrbit = (station) => { 159 | if (!station || !station.orbit) return; 160 | 161 | this.earth.remove(station.orbit); 162 | station.orbit.geometry.dispose(); 163 | station.orbit = null; 164 | station.mesh.material = this.material; 165 | this.render(); 166 | } 167 | 168 | highlightStation = (station) => { 169 | station.mesh.material = this.highlightedMaterial; 170 | } 171 | 172 | clearStationHighlight = (station) => { 173 | station.mesh.material = this.material; 174 | } 175 | 176 | setReferenceFrame = (type) => { 177 | this.referenceFrame = type; 178 | } 179 | 180 | _addTleFileStations = (lteFileContent, color, stationOptions) => { 181 | const stations = parseTleFile(lteFileContent, stationOptions); 182 | 183 | const { satelliteSize } = stationOptions; 184 | 185 | stations.forEach(s => { 186 | this.addSatellite(s, color, satelliteSize); 187 | }); 188 | 189 | this.render(); 190 | 191 | return stations; 192 | } 193 | 194 | 195 | 196 | _getSatelliteMesh = (color, size) => { 197 | color = color || this.options.defaultSatelliteColor; 198 | size = size || SatelliteSize; 199 | 200 | if (!this.geometry) { 201 | 202 | this.geometry = new THREE.BoxBufferGeometry(size, size, size); 203 | this.material = new THREE.MeshPhongMaterial({ 204 | color: color, 205 | emissive: 0xFF4040, 206 | flatShading: false, 207 | side: THREE.DoubleSide, 208 | }); 209 | } 210 | 211 | return new THREE.Mesh(this.geometry, this.material); 212 | } 213 | 214 | _setupSpriteMaterials = (color) => { 215 | if (this.material && this.lastColor === color) return; 216 | 217 | this._satelliteSprite = new THREE.TextureLoader().load(circle, this.render); 218 | this.selectedMaterial = new THREE.SpriteMaterial({ 219 | map: this._satelliteSprite, 220 | color: 0xFF0000, 221 | sizeAttenuation: false 222 | }); 223 | this.highlightedMaterial = new THREE.SpriteMaterial({ 224 | map: this._satelliteSprite, 225 | color: 0xfca300, 226 | sizeAttenuation: false 227 | }); 228 | this.material = new THREE.SpriteMaterial({ 229 | map: this._satelliteSprite, 230 | color: color, 231 | sizeAttenuation: false 232 | }); 233 | this.lastColor = color; 234 | } 235 | 236 | _getSatelliteSprite = (color, size) => { 237 | const SpriteScaleFactor = 5000; 238 | 239 | this._setupSpriteMaterials(color); 240 | 241 | const result = new THREE.Sprite(this.material); 242 | result.scale.set(size / SpriteScaleFactor, size / SpriteScaleFactor, 1); 243 | return result; 244 | } 245 | 246 | _getSatellitePositionFromTle = (station, date) => { 247 | date = date || TargetDate; 248 | return getPositionFromTle(station, date, this.referenceFrame); 249 | } 250 | 251 | updateSatellitePosition = (station, date) => { 252 | date = date || TargetDate; 253 | 254 | const pos = getPositionFromTle(station, date, this.referenceFrame); 255 | if (!pos) return; 256 | 257 | station.mesh.position.set(pos.x, pos.y, pos.z); 258 | } 259 | 260 | 261 | updateAllPositions = (date) => { 262 | if (!this.stations) return; 263 | 264 | this.stations.forEach(station => { 265 | this.updateSatellitePosition(station, date); 266 | }); 267 | 268 | if (this.referenceFrame === 2) 269 | this._updateEarthRotation(date); 270 | else 271 | this.render(); 272 | } 273 | 274 | _updateEarthRotation = (date) => { 275 | const gst = satellite.gstime(date) 276 | this.earthMesh.setRotationFromEuler(new THREE.Euler( 0, gst, 0)); 277 | 278 | this.render(); 279 | } 280 | 281 | 282 | // __ Scene _______________________________________________________________ 283 | 284 | 285 | _setupScene = () => { 286 | const width = this.el.clientWidth; 287 | const height = this.el.clientHeight; 288 | 289 | this.scene = new THREE.Scene(); 290 | 291 | this._setupCamera(width, height); 292 | 293 | this.renderer = new THREE.WebGLRenderer({ 294 | logarithmicDepthBuffer: true, 295 | antialias: true 296 | }); 297 | 298 | this.renderer.setClearColor(new THREE.Color(this.options.backgroundColor)); 299 | this.renderer.setSize(width, height); 300 | 301 | this.el.appendChild(this.renderer.domElement); 302 | }; 303 | 304 | _setupCamera(width, height) { 305 | var NEAR = 1e-6, FAR = 1e27; 306 | this.camera = new THREE.PerspectiveCamera(54, width / height, NEAR, FAR); 307 | this.controls = new OrbitControls(this.camera, this.el); 308 | this.controls.enablePan = false; 309 | this.controls.addEventListener('change', () => this.render()); 310 | this.camera.position.z = -15000; 311 | this.camera.position.x = 15000; 312 | this.camera.lookAt(0, 0, 0); 313 | } 314 | 315 | _setupLights = () => { 316 | const sun = new THREE.PointLight(0xffffff, 1, 0); 317 | //sun.position.set(0, 0, -149400000); 318 | sun.position.set(0, 59333894, -137112541); 319 | 320 | const ambient = new THREE.AmbientLight(0x909090); 321 | 322 | this.scene.add(sun); 323 | this.scene.add(ambient); 324 | } 325 | 326 | _addBaseObjects = () => { 327 | this._addEarth(); 328 | }; 329 | 330 | render = () => { 331 | this.renderer.render(this.scene, this.camera); 332 | //this.requestID = window.requestAnimationFrame(this._animationLoop); 333 | }; 334 | 335 | 336 | 337 | // __ Scene contents ______________________________________________________ 338 | 339 | 340 | _addEarth = () => { 341 | const textLoader = new THREE.TextureLoader(); 342 | 343 | const group = new THREE.Group(); 344 | 345 | // Planet 346 | let geometry = new THREE.SphereGeometry(earthRadius, 50, 50); 347 | let material = new THREE.MeshPhongMaterial({ 348 | //color: 0x156289, 349 | //emissive: 0x072534, 350 | side: THREE.DoubleSide, 351 | flatShading: false, 352 | map: textLoader.load(earthmap, this.render) 353 | }); 354 | 355 | this.earthMesh = new THREE.Mesh(geometry, material); 356 | group.add(this.earthMesh); 357 | 358 | // // Axis 359 | // material = new THREE.LineBasicMaterial({color: 0xffffff}); 360 | // geometry = new THREE.Geometry(); 361 | // geometry.vertices.push( 362 | // new THREE.Vector3(0, -7000, 0), 363 | // new THREE.Vector3(0, 7000, 0) 364 | // ); 365 | 366 | // var earthRotationAxis = new THREE.Line(geometry, material); 367 | // group.add(earthRotationAxis); 368 | 369 | this.earth = group; 370 | this.scene.add(this.earth); 371 | 372 | } 373 | 374 | _findStationFromMesh = (threeObject) => { 375 | for (var i = 0; i < this.stations.length; ++i) { 376 | const s = this.stations[i]; 377 | 378 | if (s.mesh === threeObject) return s; 379 | } 380 | 381 | return null; 382 | } 383 | } --------------------------------------------------------------------------------