-
Notifications
You must be signed in to change notification settings - Fork 4
/
app.js
203 lines (172 loc) · 4.87 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
const express = require("express");
const fetch = require("node-fetch");
const satellite = require("satellite.js");
const Bottleneck = require("bottleneck/es5");
const predict = require("./predict");
const cache = require("./cache");
const PORT = process.env.PORT || 3000;
const MS_PER_HOUR = 3600000;
const ISS_TRACK_INTERVAL_MS = 100;
const SATELLITE_DATA_API_URL = "https://celestrak.org";
const LOCATION_DATA_API_URL = "https://nominatim.openstreetmap.org";
const headers = {headers: {"User-Agent": "issviewer"}};
const limiter = new Bottleneck({
maxConcurrent: 1,
minTime: 1000
});
const app = express();
let tleData = [];
let issPosition = {};
app.use(express.static("./dist"));
function tleStringToArray(tleString)
{
const arr = tleString.split("\n");
return [arr[0], arr[1], arr[2]];
}
function tleArrayToString(arr)
{
return `${arr[0]}\n${arr[1]}\n${arr[2]}`;
}
function updatePosition()
{
if (tleData.length > 0)
issPosition = getCurrentPosition();
}
// current position of the ISS
function getCurrentPosition()
{
const satrec = satellite.twoline2satrec(tleData[1], tleData[2]);
const posvel = satellite.propagate(satrec, new Date());
const posEci = posvel.position;
const gmst = satellite.gstime(new Date());
const posGd = satellite.eciToGeodetic(posEci, gmst);
const lat = satellite.degreesLat(posGd.latitude);
const lon = satellite.degreesLong(posGd.longitude);
const height = posGd.height;
const data =
{
lat: Number(lat.toFixed(2)),
lon: Number(lon.toFixed(2)),
height: Number(height.toFixed(1)),
velocityKmph: Math.round(eciVelocityToKmph(posvel.velocity))
};
return data;
}
async function startLoadTleData()
{
try
{
const data = await cache.getTle();
if (data != null)
{
const cachedTleArray = tleStringToArray(data);
const cachedTleYear = parseInt(cachedTleArray[1].substring(18, 20));
const cachedTleDay = parseFloat(cachedTleArray[1].substring(20, 32));
const utcNowString = new Date().toUTCString();
const utcNow = new Date(utcNowString);
const currentFullYear = utcNow.getFullYear();
const currentTleYear = parseInt(currentFullYear.toString().substring(2, 4)); // current year in TLE format
const differenceMs = utcNow - new Date(currentFullYear, 0, 0);
const currentTleDay = differenceMs / (1000*60*60*24); // current day in TLE format
// check if cached TLE is recent enough
if (cachedTleYear == currentTleYear && currentTleDay - cachedTleDay < 1)
{
console.log("cached TLE is recent enough and will be used");
tleData = cachedTleArray;
} else
{
console.log("cached TLE is old and will be redownloaded");
fetchTleData();
}
} else
{
console.log("TLE wasn't found in cache and will be downloaded");
fetchTleData();
}
} catch (err)
{
console.error("Can't get TLE data: ", err);
}
}
async function fetchTleData()
{
try
{
const requestUrl = `${SATELLITE_DATA_API_URL}/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle`;
const response = await fetch(requestUrl, headers);
const fullTleFile = await response.text();
tleData = tleStringToArray(fullTleFile);
// cache TLE string
cache.saveTle(tleArrayToString(tleData));
} catch (err)
{
console.error("Can't get TLE data: ", err);
}
}
function eciVelocityToKmph(velocity)
{
return Math.sqrt(
Math.pow(velocity.x * 3600, 2) + Math.pow(velocity.y * 3600, 2) + Math.pow(velocity.z * 3600, 2)
);
}
// json response containing list of predictions and location data
async function predictResponse (req, res)
{
try
{
const { locationName } = req.params;
if (!locationName)
{
res.json({error: "location name was not provided"});
return;
}
let location = await cache.getLocation(locationName);
// location not in cache, download it
if (location == null)
{
console.log("location is not in cache and will be downloaded");
const requestUrl = encodeURI(`${LOCATION_DATA_API_URL}/search?format=json&q=${locationName}`);
const response = await limiter.schedule(() =>
fetch(requestUrl, headers)
);
const places = await response.json();
if (places.length > 0)
{
location = places[0];
} else
{
res.json({});
return;
}
}
const predictions = await predict.getPasses(tleData, location.lon, location.lat);
const data =
{
location:
{
lon: location.lon,
lat: location.lat
},
passes: predictions
};
// cache location data
cache.saveLocation(locationName, data.location);
res.json(data);
} catch (err)
{
console.error("Can't get location data: ", err);
res.status(500).send("Internal Server Error");
}
}
app.get("/track", (req, res) =>
{
res.json(issPosition);
});
app.get("/predict/:locationName", predictResponse);
app.listen(PORT, () =>
{
console.log(`Listening on ${PORT}`);
});
startLoadTleData();
setInterval(fetchTleData, MS_PER_HOUR); // fetch new TLE hourly
setInterval(updatePosition, ISS_TRACK_INTERVAL_MS); // calculate and store current position periodically