Starlink: React JS based Starlink Trajectory Visualization
1 Introdcution
- Designed and developed a visualization dashboard using ReactJS and D3 to track satellites in real-time based on geo-location.
- Built location, altitude, and duration based selector to refine satellite search.
- Animated selected satellite paths on a world map using D3 to improve the user-friendliness.
- Deployed the dashboard to Amazon Web Service for demonstration.
1.1 What is SpaceX project about
https://www.starlink.com/
1.2 Where can we get the dataset
https://www.n2yo.com/api/#positions
2 Project Set Up
2.1 Get a key
Register on N2Yo : https://www.n2yo.com/login/register/
2.2 Get an IDE
Webstorm or IntelliJ or others (vs code)
2.3 Create SpaceX project
Method1 (recommended) :
Method 2:
2.4 Project Structure
Create folders.
2.5 Move files to folders
3 Steps - Part 1
3.1 Download starlink logo and save to src/assets/images
https://stackblitz.com/edit/react-starlink1?file=src%2Fassets%2Fimages%2Fspacex_logo.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" x="0px" y="0px" viewBox="0 0 400 50"
xmlns="http://www.w3.org/2000/svg" >
<title>SpaceX Logo</title>
<g class="letter_s">
<path fill="white" d="M37.5,30.5H10.9v-6.6h34.3c-0.9-2.8-3.8-5.4-8.9-5.4H11.4c-5.7,0-9,2.1-9,6.7v4.9c0,4,3.4,6.3,8.4,6.3h26.9v7H1.5
c0.9,3.8,3.8,5.8,9,5.8h27.1c5.7,0,8.5-2.2,8.5-6.9v-4.9C46.1,33.1,42.8,30.8,37.5,30.5z"></path>
</g>
<g class="letter_p">
<path fill="white" d="M91.8,18.6H59v30.7h9.3V37.5h24.2c6.7,0,10.4-2.3,10.4-7.7v-3.4C102.8,21.4,98.6,18.6,91.8,18.6z M94.8,28.4
c0,2.2-0.4,3.4-4,3.4H68.3l0.1-8h22c4,0,4.5,1.2,4.5,3.3V28.4z"></path>
</g>
<g class="letter_a">
<polygon fill="white" points="129.9,17.3 124.3,24.2 133.8,37.3 114,37.3 109.1,42.5 137.7,42.5 142.6,49.3 153.6,49.3 "></polygon>
</g>
<g class="letter_c">
<path fill="white" d="M171.4,23.9h34.8c-0.9-3.6-4.4-5.4-9.4-5.4h-26c-4.5,0-8.8,1.8-8.8,6.7v17.2c0,4.9,4.3,6.7,8.8,6.7h26.3
c6,0,8.1-1.7,9.1-5.8h-34.8V23.9z"></path>
</g>
<g class="letter_e">
<polygon fill="white" points="228.3,43.5 228.3,34.1 247,34.1 247,28.9 218.9,28.9 218.9,49.3 260.4,49.3 260.4,43.5 "></polygon>
<rect fill="white" x="219.9" y="18.6" width="41.9" height="5.4"></rect>
</g>
<g class="letter_x">
<path fill="white" d="M287.6,18.6H273l17.2,12.6c2.5-1.7,5.4-3.5,8-5L287.6,18.6z"></path>
<path fill="white" d="M308.8,34.3c-2.5,1.7-5,3.6-7.4,5.4l13,9.5h14.7L308.8,34.3z"></path>
</g>
<g class="letter_swoosh">
<path fill="white" d="M399,0.7c-80,4.6-117,38.8-125.3,46.9l-1.7,1.6h14.8C326.8,9.1,384.3,2,399,0.7L399,0.7z"></path>
</g>
</svg>
3.2 Update index.js (update import path for App)
... ...
import App from './components/App';
... ...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
......
3.3 Create Header.js Component
import React, {Component} from 'react';
import spacex_logo from '../assets/images/spacex_logo.svg';
class Header extends Component {
render() {
return (
<header className="App-header">
<img src={spacex_logo} className="App-logo" alt="logo" />
<p className="title">
StarLink Tracker
</p>
</header>
);
}
}
export default Header;
3.4 Add Header.js to App.js
import React from 'react';
import Header from './Header';
function App() {
return (
<div className="App">
<Header />
</div>
);
}
export default App;
3.5 Add App.css to index.css
@import "./styles/App.css";
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
3.6 Update App.css (override whole file)
.App {
text-align: center;
min-height: 100vh;
overflow: hidden;
display: block;
position: relative;
padding-bottom: 5px;
}
3.7 Create Header.css and import it to index.css
3.8 Update Header.css
.App-logo {
height: 28px;
margin-left: 20px;
margin-bottom: 10px;
}
.App-header {
display: flex;
align-items: center;
height: 70px;
width: 100vw;
color: #fff;
background-color: #00305C;
box-shadow: rgb(220, 220, 220) 0px 2px 10px;
}
.title {
font-size: 20px;
font-weight: bold;
margin: 0px;
}
3.9 Create Footer.js Component
import React, {Component} from 'react';
class Footer extends Component {
render() {
return (
<footer className="footer">
©2020 StarLink Tracker. All Rights Reserved. Website Made by Your name
</footer>
);
}
}
export default Footer;
3.10 Add Footer.js to App.js
import React from 'react';
import Header from './Header';
import Footer from './Footer';
function App() {
return (
<div className="App">
<Header />
<Footer />
</div>
);
}
export default App;
3.11 Create Footer.css and add it to index.css
3.12 Update Footer.css
.footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: 100%;
text-align: center;
}
npm start
3.13 install AntDesign
https://ant.design/docs/react/introduce
npm install --save antd
3.14 Add antd css in index.css (at the very top of the file)
After 2023 https://ant.design/docs/react/migration-v5
3.15 Create Main.js Component
import React, {Component} from 'react';
import { Row, Col } from 'antd';
class Main extends Component {
render() {
return (
<Row>
main
</Row>
);
}
}
export default Main;
3.16 Create Main.css and add it to index.css
Leave Main.css empty for now
3.17 Add Main.js to App.js
import React from 'react';
import Header from './Header';
import Main from './Main';
import Footer from './Footer';
function App() {
return (
<div className="App">
<Header />
<Main />
<Footer />
</div>
);
}
export default App;
3.18 Update Main.js
3.19 Update Main.css
3.20 Create SatSetting.js Component
import React, {Component} from 'react';
class SatSetting extends Component {
render() {
return (
<div>
SatSetting
</div>
);
}
}
export default SatSetting;
3.21 Add SatSetting.js Component to Main.js
import React, {Component} from 'react';
import { Row, Col } from 'antd';
import SatSetting from './SatSetting';
class Main extends Component {
render() {
return (
<Row className='main'>
<Col span={8} className="left-side"> // 占8个单位
<SatSetting />
</Col>
<Col span={16} className="right-side"> // 占16个单位 左右1:2 的关系
right
</Col>
</Row>
);
}
}
export default Main;
https://ant.design/components/grid
3.22 Update SatSetting.js Component
import React, {Component} from 'react';
import {Form, Button, InputNumber} from 'antd';
class SatSetting extends Component {
showSatellite = values => {
console.log('Received values of form: ', values);
}
render() {
return (
<Form
name="wrap"
labelCol={{
flex: '110px',
}}
labelAlign="left"
labelWrap
wrapperCol={{
flex: 1,
}}
colon={false}
className="sat-setting"
onFinish={this.showSatellite}
>
<Form.Item
label="Longitude(degrees)"
name="longitude"
rules={[
{
required: true,
message: "Please input your Longitude",
}
]}
>
<InputNumber min={-180} max={180}
style={{width: "100%"}}
placeholder="Please input Longitude"
/>
</Form.Item>
<Form.Item
label="Latitude(degrees)"
name="latitude"
rules={[
{
required: true,
message: "Please input your Latitude",
}
]}
>
<InputNumber min={-90} max={90}
style={{width: "100%"}}
placeholder="Please input Latitude"
/>
</Form.Item>
<Form.Item
label="Altitude(meters)"
name="elevation"
rules={[
{
required: true,
message: "Please input your Altitude",
}
]}
>
<InputNumber min={-413} max={8850}
style={{width: "100%"}}
placeholder="Please input Altitude"
/>
</Form.Item>
<Form.Item
label="Radius(degrees)"
name="altitude"
rules={[
{
required: true,
message: "Please input your Radius",
}
]}
>
<InputNumber min={0} max={90}
style={{width: "100%"}}
placeholder="Please input Radius"
/>
</Form.Item>
<Form.Item
label="Duration(secs)"
name="duration"
rules={[
{
required: true,
message: "Please input your Duration",
}
]}
>
<InputNumber min={0} max={90}
style={{width: "100%"}}
placeholder="Please input Duration"
/>
</Form.Item>
<Form.Item className="show-nearby">
<Button type="primary" htmlType="submit" style={{textAlign: "center"}}>
Find Satellite
</Button>
</Form.Item>
</Form>
);
}
}
export default SatSetting;
3.23 Create SatSetting.css and add it to index.css
.sat-setting {
width: 100%;
background-color: #fbfbfb;
border: 1px solid #d9d9d9;
padding: 10px 10px 0px 5px;
text-align: left;
}
.show-nearby {
display: flex;
justify-content: center;
}
export default Main; 这些要大写
Steps - Part 2
- Create
SatelliteList.js
component
- Update
Main.js
import React, {Component} from "react";
import { Row, Col } from 'antd';
import SatSetting from './SatSetting';
import SatelliteList from './SatelliteList';
class Main extends Component{
render() {
return (
<Row className='main'>
<Col span={8} className='left-side'>
<SatSetting />
<SatelliteList />
</Col>
<Col span={16} className='right-side'>
right
</Col>
</Row>
);
}
}
export default Main;
- Create StatelliteList.css and add it to
index.css
@import "./styles/SatSetting.css";
in SatelliteList.css
.sat-list-box {
border: 2px solid #ccc;
height: 380px;
margin-top: 10px;
padding: 10px;
}
.btn-container {
text-align: center;
}
.sat-list {
height: 300px;
overflow: scroll;
}
.spin-box{
display: flex;
align-items: center;
justify-content: center;
height: 335px;
}
- Install axios library
https://github.com/axios/axios
npm install --save axios
- Configure development - server
https://create-react-app.dev/docs/deployment/
https://create-react-app.dev/docs/proxying-api-requests-in-development/
Update package.json
{
"name": "starlink",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"antd": "^5.9.2",
"axios": "^1.6.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "https://api.n2yo.com"
}
- Create constants.js under src folder
https://www.n2yo.com/login/edit/
export const SAT_API_KEY = "YOUR KEY";
export const STARLINK_CATEGORY = "52";
export const NEARBY_SATELLITE = "rest/v1/satellite/above";
export const BASE_URL = ""; // your backend endpoint when deloy
- Update
Main.js
import React, {Component} from "react";
import { Row, Col } from 'antd';
import axios from "axios";
import SatSetting from './SatSetting';
import SatelliteList from './SatelliteList';
import { SAT_API_KEY, BASE_URL, NEARBY_SATELLITE, STARLINK_CATEGORY} from "../constants";
/*
js中的import的格式是: import {模块中的方法} from '模块的路径'
其中模块的路径可以是相对路径,也可以是绝对路径,但是一定要加上模块的后缀名,否则会报错。
{} 里面的方法是模块中导出的方法,如果是默认导出的方法,那么就不需要加上{},直接导入就可以了。
import React, {Component} from "react"; 从react模块中导入React和Component方法。
React是一个类,Component是一个方法。
import {Row, Col } from 'antd'; 从antd模块中导入Row和Col方法。
*/
class Main extends Component{
state = {
setting: {},
satInfo: {},
satList: [],
isLoadingList: false
}
showNearbySatellite = (setting) => {
console.log('show nearby')
this.setState({
setting: setting
})
this.fetchSatellite(setting);
}
fetchSatellite = (setting) => {
console.log("fetching")
const { latitude, longitude, elevation, altitude } = setting;
const url = `${BASE_URL}/${NEARBY_SATELLITE}/${latitude}/${longitude}/${elevation}/${altitude}/${STARLINK_CATEGORY}/$&apiKey=${SAT_API_KEY}`;
this.setState({
isLoadingList: true
});
axios.get(url)
.then( res => {
console.log(res.data);
this.setState({
satInfo: res.data,
isLoadingList: false
})
})
.catch( error => {
this.setState({
isLoadingList: false
});
console.log('err in fetch satellite -> ', error);
})
}
render() {
const { satInfo, isLoadingList, satList, setting } = this.state;
return (
<Row className='main'>
<Col span={8} className='left-side'>
<SatSetting />
<SatelliteList />
</Col>
<Col span={16} className='right-side'>
right
</Col>
</Row>
);
}
}
export default Main;
/*
class Main extends Component { ... } 定义了一个名为 Main 的类组件。
extends Component 表示 Main 组件继承了 Component 类。
render() { ... } 定义了一个名为 render 的方法,这个方法返回一个 React 元素。
<Row> ... </Row> 是一个 React 元素,它是一个 HTML 元素,它的类型是 Row。
Row类型的元素有一个子元素,这个子元素是 main。
<Col span={8} className='left-side'> ... </Col> 是一个 React 元素,
它是一个 HTML 元素,它的类型是 Col,它有两个属性,分别是 span 和 className。
span 属性的值是 8,className 属性的值是 left-side。
<Col span={8} className='left-side'> ... </Col> 的内容是 left。
SatSetting 是一个 React 元素,它是一个自定义组件,它的类型是 SatSetting。
- Update
SateSetting.js
import React, {Component} from 'react';
import {Form, Button, InputNumber} from 'antd';
class SatSetting extends Component {
showSatellite = values => {
console.log('Received values of form: ', values);
this.props.onShow(values); // here
}
render() {
return (
<Form
name="wrap"
labelCol={{
flex: '110px',
}}
labelAlign="left"
labelWrap
wrapperCol={{
flex: 1,
}}
colon={false}
className="sat-setting"
onFinish={this.showSatellite}
>
<Form.Item
label="Longitude(degrees)"
name="longitude"
rules={[
{
required: true,
message: "Please input your Longitude",
}
]}
>
<InputNumber min={-180} max={180}
style={{width: "100%"}}
placeholder="Please input Longitude"
/>
</Form.Item>
<Form.Item
label="Latitude(degrees)"
name="latitude"
rules={[
{
required: true,
message: "Please input your Latitude",
}
]}
>
<InputNumber min={-90} max={90}
style={{width: "100%"}}
placeholder="Please input Latitude"
/>
</Form.Item>
<Form.Item
label="Altitude(meters)"
name="elevation"
rules={[
{
required: true,
message: "Please input your Altitude",
}
]}
>
<InputNumber min={-413} max={8850}
style={{width: "100%"}}
placeholder="Please input Altitude"
/>
</Form.Item>
<Form.Item
label="Radius(degrees)"
name="altitude"
rules={[
{
required: true,
message: "Please input your Radius",
}
]}
>
<InputNumber min={0} max={90}
style={{width: "100%"}}
placeholder="Please input Radius"
/>
</Form.Item>
<Form.Item
label="Duration(secs)"
name="duration"
rules={[
{
required: true,
message: "Please input your Duration",
}
]}
>
<InputNumber min={0} max={90}
style={{width: "100%"}}
placeholder="Please input Duration"
/>
</Form.Item>
<Form.Item className="show-nearby">
<Button type="primary" htmlType="submit" style={{textAlign: "center"}}>
Find Satellite
</Button>
</Form.Item>
</Form>
);
}
}
export default SatSetting;
- Update satelliteList.js
Copy the svg from
https://stackblitz.com/edit/react-starlink2?file=src%2Fassets%2Fimages%2Fsatellite.svg
import React, {Component} from 'react';
import { List, Avatar, Button, Checkbox, Spin } from 'antd';
import satellite from "../assets/images/statellite.svg";
class SatelliteList extends Component {
render() {
const satList = this.props.satInfo ? this.props.satInfo.above : [];
const { isLoad } = this.props;
return (
<div className="sat-list-box">
<Button className="sat-list-btn" type="primary">Track</Button>
<hr/>
{
isLoad ?
<div className="spin-box">
<Spin tip="Loading..." size="large" />
</div>
:
<List
className="sat-list"
itemLayout="horizontal"
size="small"
dataSource={satList}
renderItem={item => (
<List.Item
actions={[<Checkbox dataInfo={item}onCharge={this.onCharge}/>]}
>
<List.Item.Meta
avatar={<Avatar size={50} src={satellite}/>
}
title={<p>{item.satname}</p>}
description={`Launch Date:$(item.lauchDate)`}
/>
</List.Item>
)}
/>
}
</div>
);
}
}
export default SatelliteList;
-
Update SatelliteList.css
Steps - Part 3
- Upate SatelliteList.js
import React, {Component} from 'react';
import { List, Avatar, Button, Checkbox, Spin } from 'antd';
import satellite from "../assets/images/statellite.svg";
class SatelliteList extends Component {
constructor(){
super();
this.state = {
selected:[]
};
}
onChange = e => {
const { dataInfo, checked } = e.target;
const { selected } = this.state;
const list = this.addOrRemove(dataInfo, checked, selected);
this.setState({ selected: list })
}
addOrRemove = (item, status, list) => {
const found = list.some( entry => entry.satid === item.satid);
if(status && !found){
list=[...list, item]
}
if(!status && found){
list = list.filter( entry => {
return entry.satid !== item.satid;
});
}
return list;
}
onShowSatMap = () => {
this.props.onShowMap(this.state.selected);
}
render() {
const satList = this.props.satInfo ? this.props.satInfo.above : [];
const { isLoad } = this.props;
return (
<div className="sat-list-box">
<Button className="sat-list-btn" type="primary">Track</Button>
<hr/>
{
isLoad ?
<div className="spin-box">
<Spin tip="Loading..." size="large" />
</div>
:
<List
className="sat-list"
itemLayout="horizontal"
size="small"
dataSource={satList}
renderItem={item => (
<List.Item
actions={[<Checkbox dataInfo={item}onCharge={this.onCharge}/>]}
>
<List.Item.Meta
avatar={<Avatar size={50} src={satellite}/>
}
title={<p>{item.satname}</p>}
description={`Launch Date:$(item.lauchDate)`}
/>
</List.Item>
)}
/>
}
</div>
);
}
}
export default SatelliteList;
- Update
SatellitList.css
.sat-list-box {
border: 2px solid #ccc;
height: 380px;
margin-top: 10px;
padding: 10px;
}
.btn-container {
text-align: center;
}
.sat-list {
height: 310px;
overflow: scroll;
}
.spin-box{
display: flex;
align-items: center;
justify-content: center;
height: 320px;
}
- Update
Main.js
import React, {Component} from "react";
import { Row, Col } from 'antd';
import axios from "axios";
import SatSetting from './SatSetting';
import SatelliteList from './SatelliteList';
import { SAT_API_KEY, BASE_URL, NEARBY_SATELLITE, STARLINK_CATEGORY} from "../constants";
class Main extends Component{
state = {
setting: {},
satInfo: {},
satList: [],
isLoadingList: false
}
showNearbySatellite = (setting) => {
console.log('show nearby')
this.setState({
setting: setting
})
this.fetchSatellite(setting);
}
fetchSatellite = (setting) => {
console.log("fetching")
const { latitude, longitude, elevation, altitude } = setting;
const url = `${BASE_URL}/${NEARBY_SATELLITE}/${latitude}/${longitude}/${elevation}/${altitude}/${STARLINK_CATEGORY}/$&apiKey=${SAT_API_KEY}`;
this.setState({
isLoadingList: true
});
axios.get(url)
.then( res => {
console.log(res.data);
this.setState({
satInfo: res.data,
isLoadingList: false
})
})
.catch( error => {
this.setState({
isLoadingList: false
});
console.log('err in fetch satellite -> ', error);
})
}
showMap = (data) => {
console.log('show on the map -> ', data);
}
render() {
const { satInfo, isLoadingList, satList, setting } = this.state;
return (
<Row className='main'>
<Col span={8} className='left-side'>
<SatSetting onShow={this.showNearbySatellite} />
<SatelliteList satInfo={satInfo}
isLoad={isLoadingList}
onShowMap={this.showMap}
/>
</Col>
<Col span={16} className='right-side'>
right
</Col>
</Row>
);
}
}
export default Main;
- Create WorldMap.js
import React, {Component} from 'react'
class WorldMap extends Component {
redner() {
return (
<div>
world map
</div>
)
}
}
export default WorldMap;
- Update Main.js
import React, {Component} from "react";
import { Row, Col } from 'antd';
import axios from "axios";
import SatSetting from './SatSetting';
import SatelliteList from './SatelliteList';
import { SAT_API_KEY, BASE_URL, NEARBY_SATELLITE, STARLINK_CATEGORY} from "../constants";
import WorldMap from './WorldMap'; //
class Main extends Component{
state = {
setting: {},
satInfo: {},
satList: [],
isLoadingList: false
}
showNearbySatellite = (setting) => {
console.log('show nearby')
this.setState({
setting: setting
})
this.fetchSatellite(setting);
}
fetchSatellite = (setting) => {
console.log("fetching")
const { latitude, longitude, elevation, altitude } = setting;
const url = `${BASE_URL}/${NEARBY_SATELLITE}/${latitude}/${longitude}/${elevation}/${altitude}/${STARLINK_CATEGORY}/$&apiKey=${SAT_API_KEY}`;
this.setState({
isLoadingList: true
});
axios.get(url)
.then( res => {
console.log(res.data);
this.setState({
satInfo: res.data,
isLoadingList: false
})
})
.catch( error => {
this.setState({
isLoadingList: false
});
console.log('err in fetch satellite -> ', error);
})
}
//
showMap = (data) => {
console.log('show on the map -> ', data);
}
render() {
const { satInfo } = this.state;
return (
<Row className='main'>
<Col span={8} className='left-side'>
<SatSetting onShow={this.showNearbySatellite} />
<SatelliteList satInfo={satInfo}
isLoad={this.state.isLoadingList}
onShowMap={this.showMap}
/>
</Col>
<Col span={16} className='right-side'>
<WorldMap />
</Col>
</Row>
);
}
}
export default Main;
- Install libraries
-
topojson: https://github.com/topojson/topojson-client
npm install --save topojson-client
-
d3-geo: https://github.com/d3/d3-geo
npm install --save d3-geo
-
d3-geo-projection: https://github.com/d3/d3-geo-projection
npm install --save d3-geo-projection
-
d3-selection: https://github.com/d3/d3-selection
npm install --save d3-selection
- Update
constants.js
export const SAT_API_KEY = "LT6DRG-NGLYGH-TCY8HW-54GV"; // "YOUR KEY";
export const STARLINK_CATEGORY = "52";
export const NEARBY_SATELLITE = "rest/v1/satellite/above";
export const WORLD_MAP_URL = "https://unpkg.com/world-atlas@1/world/110m.json";
export const BASE_URL = ""; // your backend endpoint when deloy
- Update WorldMap.js
import React, { Component } from "react";
import axios from "axios";
import { Spin } from "antd";
import { feature } from "topojson-client";
import { geoKavrayskiy7 } from "d3-geo-projection";
import { geoGraticule, geoPath } from "d3-geo";
import { select as d3Select } from "d3-selection";
import { schemeCategory10 } from "d3-scale-chromatic";
import * as d3Scale from "d3-scale";
import { timeFormat as d3TimeFormat } from "d3-time-format";
import {
WORLD_MAP_URL,
SATELLITE_POSITION_URL,
SAT_API_KEY
} from "../constants";
const width = 960;
const height = 600;
class WorldMap extends Component {
constructor() {
super();
this.state = {
isLoading: false,
isDrawing: false
};
this.map = null;
this.color = d3Scale.scaleOrdinal(schemeCategory10);
this.refMap = React.createRef();
this.refTrack = React.createRef();
}
componentDidMount() {
axios
.get(WORLD_MAP_URL)
.then(res => {
const { data } = res;
const land = feature(data, data.objects.countries).features;
this.generateMap(land);
})
.catch(e => {
console.log("err in fetch map data ", e.message);
});
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevProps.satData !== this.props.satData) {
const {
latitude,
longitude,
elevation,
altitude,
duration
} = this.props.observerData;
const endTime = duration * 60;
this.setState({
isLoading: true
});
const urls = this.props.satData.map(sat => {
const { satid } = sat;
const url = `/api/${SATELLITE_POSITION_URL}/${satid}/${latitude}/${longitude}/${elevation}/${endTime}/&apiKey=${SAT_API_KEY}`;
return axios.get(url);
});
Promise.all(urls)
.then(res => {
const arr = res.map(sat => sat.data);
this.setState({
isLoading: false,
isDrawing: true
});
if (!prevState.isDrawing) {
this.track(arr);
} else {
const oHint = document.getElementsByClassName("hint")[0];
oHint.innerHTML =
"Please wait for these satellite animation to finish before selection new ones!";
}
})
.catch(e => {
console.log("err in fetch satellite position -> ", e.message);
});
}
}
track = data => {
if (!data[0].hasOwnProperty("positions")) {
throw new Error("no position data");
return;
}
const len = data[0].positions.length;
const { duration } = this.props.observerData;
const { context2 } = this.map;
let now = new Date();
let i = 0;
let timer = setInterval(() => {
let ct = new Date();
let timePassed = i === 0 ? 0 : ct - now;
let time = new Date(now.getTime() + timePassed);
context2.clearRect(0, 0, width, height);
context2.font = "bold 14px sans-serif";
context2.fillStyle = "#333";
context2.textAlign = "center";
context2.fillText(d3TimeFormat(time), width / 2, 10);
if (i >= len) {
clearInterval(timer);
this.setState({ isDrawing: false });
const oHint = document.getElementsByClassName("hint")[0];
oHint.innerHTML = "";
return;
}
data.forEach(sat => {
const { info, positions } = sat;
this.drawSat(info, positions[i]);
});
i += 60;
}, 1000);
};
drawSat = (sat, pos) => {
const { satlongitude, satlatitude } = pos;
if (!satlongitude || !satlatitude) return;
const { satname } = sat;
const nameWithNumber = satname.match(/\d+/g).join("");
const { projection, context2 } = this.map;
const xy = projection([satlongitude, satlatitude]);
context2.fillStyle = this.color(nameWithNumber);
context2.beginPath();
context2.arc(xy[0], xy[1], 4, 0, 2 * Math.PI);
context2.fill();
context2.font = "bold 11px sans-serif";
context2.textAlign = "center";
context2.fillText(nameWithNumber, xy[0], xy[1] + 14);
};
render() {
const { isLoading } = this.state;
return (
<div className="map-box">
{isLoading ? (
<div className="spinner">
<Spin tip="Loading..." size="large" />
</div>
) : null}
<canvas className="map" ref={this.refMap} />
<canvas className="track" ref={this.refTrack} />
<div className="hint" />
</div>
);
}
generateMap = land => {
const projection = geoKavrayskiy7()
.scale(170)
.translate([width / 2, height / 2])
.precision(0.1);
const graticule = geoGraticule();
const canvas = d3Select(this.refMap.current)
.attr("width", width)
.attr("height", height);
const canvas2 = d3Select(this.refTrack.current)
.attr("width", width)
.attr("height", height);
const context = canvas.node().getContext("2d");
const context2 = canvas2.node().getContext("2d");
let path = geoPath()
.projection(projection)
.context(context);
land.forEach(ele => {
context.fillStyle = "#B3DDEF";
context.strokeStyle = "#000";
context.globalAlpha = 0.7;
context.beginPath();
path(ele);
context.fill();
context.stroke();
context.strokeStyle = "rgba(220, 220, 220, 0.1)";
context.beginPath();
path(graticule());
context.lineWidth = 0.1;
context.stroke();
context.beginPath();
context.lineWidth = 0.5;
path(graticule.outline());
context.stroke();
});
this.map = {
projection: projection,
graticule: graticule,
context: context,
context2: context2
};
};
}
export default WorldMap;
Steps - Part 4
- Update
Main.js
import React, {Component} from 'react';
import { Row, Col } from 'antd';
import axios from 'axios';
import { NEARBY_SATELLITE, SAT_API_KEY, STARLINK_CATEGORY } from '../constants';
import SatSetting from './SatSetting';
import SatelliteList from './SatelliteList';
import WorldMap from './WorldMap';
class Main extends Component {
constructor(){
super();
this.state = {
satInfo: null,
satList: null,
setting: null,
isLoadingList: false
}
}
render() {
const { isLoadingList, satInfo, satList, setting } = this.state;
return (
// grid: col-8 + col-16 = 24 in total
// parent component is Main.js instead of App.js
// SatSetting: Satellite settings on the left top
// SatelliteList: Satellite lists on the left bottom
// WorldMap: Map on the right side
<Row className='main'>
<Col span={8} >
<SatSetting onShow={this.showNearbySatellite}/>
<SatelliteList isLoad={isLoadingList}
satInfo={satInfo}
onShowMap={this.showMap} />
</Col>
<Col span={16} className="right-side">
<WorldMap satData={satList} observerData={setting} />
</Col>
</Row>
);
}
showMap = (selected) => {
this.setState(preState => ({
...preState,
satList: [...selected]
}))
}
showNearbySatellite = (setting) => {
this.setState({
isLoadingList: true,
setting: setting
})
this.fetchSatellite(setting);
}
fetchSatellite= (setting) => {
// parameter for apis
const {latitude, longitude, elevation, altitude} = setting;
// url /api/ added
const url = `/api/${NEARBY_SATELLITE}/${latitude}/${longitude}/${elevation}/${altitude}/${STARLINK_CATEGORY}/&apiKey=${SAT_API_KEY}`;
this.setState({
isLoadingList: true
});
// make request
axios.get(url)
.then(response => {
console.log(response.data)
this.setState({
satInfo: response.data,
isLoadingList: false
})
})
.catch(error => {
console.log('err in fetch satellite -> ', error);
})
}
}
export default Main;
- Add libraries
d3-time-format https://github.com/d3/d3-time-format
npm install --save d3-time-format
d3-scale https://github.com/d3/d3-scale
npm install --save d3-scale
d3-scale-chromatic https://github.com/d3/d3-scale-chromatic
npm install --save d3-scale-chromatic
- update
constants.js
export const SAT_API_KEY = "LT6DRG-NGLYGH-TCY8HW-54GV"; // "YOUR KEY";
export const STARLINK_CATEGORY = "52";
export const NEARBY_SATELLITE = "rest/v1/satellite/above";
export const WORLD_MAP_URL = "https://unpkg.com/world-atlas@1.1.4/world/110m.json";
export const SATELLITE_POSITION_URL = "rest/v1/satellite/positions";
- Update WorldMap.js
import React, { Component } from "react";
import axios from "axios";
import { Spin } from "antd";
import { feature } from "topojson-client";
import { geoKavrayskiy7 } from "d3-geo-projection";
import { geoGraticule, geoPath } from "d3-geo";
import { select as d3Select } from "d3-selection";
import { schemeCategory10 } from "d3-scale-chromatic";
import * as d3Scale from "d3-scale";
import { timeFormat as d3TimeFormat } from "d3-time-format";
import {
WORLD_MAP_URL,
SATELLITE_POSITION_URL,
SAT_API_KEY
} from "../constants";
const width = 960;
const height = 600;
class WorldMap extends Component {
constructor() {
super();
this.state = {
isLoading: false,
isDrawing: false
};
this.map = null;
this.color = d3Scale.scaleOrdinal(schemeCategory10);
this.refMap = React.createRef();
this.refTrack = React.createRef();
}
componentDidMount() {
axios
.get(WORLD_MAP_URL)
.then(res => {
const { data } = res;
const land = feature(data, data.objects.countries).features;
this.generateMap(land);
})
.catch(e => {
console.log("err in fetch map data ", e.message);
});
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevProps.satData !== this.props.satData) {
const {
latitude,
longitude,
elevation,
altitude,
duration
} = this.props.observerData;
const endTime = duration * 60;
this.setState({
isLoading: true
});
const urls = this.props.satData.map(sat => {
const { satid } = sat;
const url = `/api/${SATELLITE_POSITION_URL}/${satid}/${latitude}/${longitude}/${elevation}/${endTime}/&apiKey=${SAT_API_KEY}`;
return axios.get(url);
});
Promise.all(urls)
.then(res => {
const arr = res.map(sat => sat.data);
this.setState({
isLoading: false,
isDrawing: true
});
if (!prevState.isDrawing) {
this.track(arr);
} else {
const oHint = document.getElementsByClassName("hint")[0];
oHint.innerHTML =
"Please wait for these satellite animation to finish before selection new ones!";
}
})
.catch(e => {
console.log("err in fetch satellite position -> ", e.message);
});
}
}
track = data => {
if (!data[0].hasOwnProperty("positions")) {
throw new Error("no position data");
return;
}
const len = data[0].positions.length;
const { duration } = this.props.observerData;
const { context2 } = this.map;
let now = new Date();
let i = 0;
let timer = setInterval(() => {
let ct = new Date();
let timePassed = i === 0 ? 0 : ct - now;
let time = new Date(now.getTime() + timePassed);
context2.clearRect(0, 0, width, height);
context2.font = "bold 14px sans-serif";
context2.fillStyle = "#333";
context2.textAlign = "center";
context2.fillText(d3TimeFormat(time), width / 2, 10);
if (i >= len) {
clearInterval(timer);
this.setState({ isDrawing: false });
const oHint = document.getElementsByClassName("hint")[0];
oHint.innerHTML = "";
return;
}
data.forEach(sat => {
const { info, positions } = sat;
this.drawSat(info, positions[i]);
});
i += 60;
}, 1000);
};
drawSat = (sat, pos) => {
const { satlongitude, satlatitude } = pos;
if (!satlongitude || !satlatitude) return;
const { satname } = sat;
const nameWithNumber = satname.match(/\d+/g).join("");
const { projection, context2 } = this.map;
const xy = projection([satlongitude, satlatitude]);
context2.fillStyle = this.color(nameWithNumber);
context2.beginPath();
context2.arc(xy[0], xy[1], 4, 0, 2 * Math.PI);
context2.fill();
context2.font = "bold 11px sans-serif";
context2.textAlign = "center";
context2.fillText(nameWithNumber, xy[0], xy[1] + 14);
};
render() {
const { isLoading } = this.state;
return (
<div className="map-box">
{isLoading ? (
<div className="spinner">
<Spin tip="Loading..." size="large" />
</div>
) : null}
<canvas className="map" ref={this.refMap} />
<canvas className="track" ref={this.refTrack} />
<div className="hint" />
</div>
);
}
generateMap = land => {
const projection = geoKavrayskiy7()
.scale(170)
.translate([width / 2, height / 2])
.precision(0.1);
const graticule = geoGraticule();
const canvas = d3Select(this.refMap.current)
.attr("width", width)
.attr("height", height);
const canvas2 = d3Select(this.refTrack.current)
.attr("width", width)
.attr("height", height);
const context = canvas.node().getContext("2d");
const context2 = canvas2.node().getContext("2d");
let path = geoPath()
.projection(projection)
.context(context);
land.forEach(ele => {
context.fillStyle = "#B3DDEF";
context.strokeStyle = "#000";
context.globalAlpha = 0.7;
context.beginPath();
path(ele);
context.fill();
context.stroke();
context.strokeStyle = "rgba(220, 220, 220, 0.1)";
context.beginPath();
path(graticule());
context.lineWidth = 0.1;
context.stroke();
context.beginPath();
context.lineWidth = 0.5;
path(graticule.outline());
context.stroke();
});
this.map = {
projection: projection,
graticule: graticule,
context: context,
context2: context2
};
};
}
export default WorldMap;
- Create WordMap.css and import it to index.css
.map-box {
position: relative;
height: 100%;
width: 100%;
Overflow: scroll;
}
.map, .track, .spinner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
.map {
z-index: 0;
}
.track {
z-index: 100;
}
.spinner {
display: flex;
align-items: center;
justify-content: center;
height: 600px;
width: 960px;
}
.hint {
color: red;
font-size: 16px;
font-weight: bold;
text-align: center;
width: 100%;
}
index.css
:
@import '~antd/dist/reset.css';
/* 一定要放在第一行 ant*/
/* 来自library */
@import "./styles/App.css";
@import "./styles/Header.css";
@import "./styles/Footer.css";
@import "./styles/Main.css";
@import "./styles/SatSetting.css";
@import "./styles/SatelliteList.css";
@import "./styles/WorldMap.css";
/* @import 用于导入其他样式表。 */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
Deploy
Starlink/SpaceX project review
- Describe the Componet tree
- Describe the dataflow
- How to we get data for starlink satellites
- How do we draw on the canvas
React
- Q: Can you explain what virtual DOM is in React and how this technique can improve render performance?
A: You tell React what state you want the UI to be in, and it makes sure the DOM matches that state. Representation of a UI is kept in memory and synced with the "real" DOM by a library - ReactDOM. React DOM compares the element and its children to the previous one, and only applies the DOM updates necessary to bring the DOM to the desired state.
https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom
- Q: What is the difference between props and state in React?
A: Props are read-only, State is not read-only but you can't directly modify the State object, you will have to use the setState() method provided by React. Changes on either Props or State will trigger the re-render of the component.
- Q: Can you list some common React Component Life Cycles and explain what they do ?
A: (Understand this diagram, focus on the common ones: didMount, didUpdate. willUnmount)
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
https://reactjs.org/docs/react-component.html#the-component-lifecycle
- Q: Given the scenario, could you please implement this feature using React?
- Clarify the requirements of this feature. What does the user need, how does each module interact with the user.
- Design and think LOUD! Tell the interviewer how you wanna design this UI, to be more specific, draw the React component hierarchy out and discuss with the interviewer to finalize what each component does.
- Go for the implementation, don't panic, ask questions like "can I use ", "do we need to consider..." whenever they come to you and block the implementation.
Javascript
- Q: How many different ways we have to define a variable in Javascript?
A: Let, Const, Var(Describe the difference in these 3 categories: Scope, Reinitialization, Reference before declaration)
https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/
- Q: Could you list the key difference between Java and Javascript?
A: Java code needs to be compiled before run, JS doesn't. Java is statically typed while Javascript is dynamically typed. Java runs on JVM while JS runs on browser or nodeJs.
- Q: What is a callback function?
A: A callback is a plain JavaScript function passed to some method as an argument or option. It is a function that is to be executed after another function has finished executing, hence the name call back`. In JavaScript, functions are objects. Because of this, functions can take functions as arguments, and can be returned by other functions.
- Q: How do we get Data from the backend using JS usually?
A: By making AJAX calls, nowadays the latest web api we use for this is Fetch API https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
HTML
- Q: Explain the new
<canvas>
element in HTML5?
A: HTML5 provides the new <canvas>
element to facilitate 2D drawing. It uses the tag <canvas>
which helps in drawing graphics using Java scripts.
- Q: Which tag is for hyperlinks in HTML, and what are the common attributes of this element?
A: <a>
tag defines hyperlinks in HTML. In terms of attributes, "href" takes the value for the target web address, "target" specifies the browsing context, for instance, target="_blank" will go to the link in a new browser tab.
- Q: Describe what HTML is?
A: HTML (Hypertext Markup Language) is not a programming language. It is a markup language that tells web browsers how to structure the web pages you visit.
- Q: Block versus Inline element?
A: Block-level elements form a visible block on a page. A block-level element appears on a new line following the content that precedes it. Any content that follows a block-level element also appears on a new line. Example - DIV
Inline elements are contained within block-level elements, and surround only small parts of the document's content. (not entire paragraphs or groupings of content) An inline element will not cause a new line to appear in the document. Example - Span
CSS
- Q: Explain the conflicting rules in CSS
- A: cascade - at a very simple level this means that the order of CSS rules matters; when two rules apply that have equal specificity the one that comes that last in the CSS is the one that will be used.
- Specificity - look up the specificity table, the selector with higher specificity value wins.
- Q: What is the BOX Model in CSS?
A: Box Model (content, padding, border, margin)
- Q: What is the selector in CSS?
A: Selector (type/class/ID, attribute, Pseudo-classes and pseudo-elements, Combinators)