Commit 1e77b8b6 authored by Stephen D's avatar Stephen D
Browse files

Added code

parent 300f615f
......@@ -28,3 +28,7 @@ build/Release
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
# Satellite data
3le.txt
ham.txt
names.json
This diff is collapsed.
# multipoint-satellite-tracker
# Multipoint Satellite Tracker
Allows viable satellites to be found given multiple points on the Earth
\ No newline at end of file
This is a piece of software I wrote to allow someone to find passes of satellites based on more than one position. It allows a different minimum AoA for each point on the Earth, based on their terrain. Elevation above sea level is found through Google's API, but will always remain positive, to account for boats in the ocean.
Check it out here: [ham.scd31.com](https://ham.scd31.com)
## Screenshots
## Information
The server runs on Node.JS, using cluster to scale vertically. At the heart of the software is the calcSatellite function, which is what actually determines viable satellites. It's terribly, terribly unoptimized. For each satellite, it advances by 30 seconds at a time, up to a week in the future. This will need to be rewritten soon, as it takes upwards of 5 seconds(!) to run. I intend to estimate how long in the future the satellite will pass by based on its current orbit and elevation, and then advance that far into the future. I may also move the function to the client, which would significantly free up server resources.
## License
This code is distrubuted under the GNU GPL v3 license. See the [LICENSE.md](LICENSE.md) file.
#container {
display: flex;
}
#controls {
float: left;
overflow-y: scroll;
height: 100%;
}
#map {
width: calc(100% - 450px);
width: -moz-calc(100% - 450px);
width: calc(100% - 450px);
min-width: 250px;
height: 100%;
background-color: grey;
flex-grow: 1;
}
#footer {
}
input[type=number] {
width: 40px;
}
table tr td {
padding: 3px;
}
.glyphicon.spinning {
animation: spin 1s infinite linear;
-webkit-animation: spin2 1s infinite linear;
}
@keyframes spin {
from { transform: scale(1) rotate(0deg);}
to { transform: scale(1) rotate(360deg);}
}
@-webkit-keyframes spin2 {
from { -webkit-transform: rotate(0deg);}
to { -webkit-transform: rotate(360deg);}
}
.glyphicon-left {
margin-right: 7px;
}
<html ng-app="satTrackApp">
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/client/css/style.css">
<script type="text/javascript" src="/client/js/jquery-3.1.1.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB3FKjZo6Fe6Lg--EU1Ci7V4rHIiU9Igb4"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
<script src="https://cdn.socket.io/socket.io-1.4.5.js"></script>
<script src="/client/js/ng-map.min.js"></script>
<script src="/client/js/moment.min.js"></script>
<script type="text/javascript" src="/client/js/main.js"></script>
</head>
<body>
<div id="container" ng-controller="satTrack-ctrl">
<div id="controls">
<table border="1" ng-show="places.length > 0">
<tr>
<td><b>ID</b></td>
<td><b>Latitude</b></td>
<td><b>Longitude</b></td>
<td><b>Minimum AoA</b></td>
<td></td>
</tr>
<tr ng-repeat="point in places">
<td>{{$index + 1}}</td>
<td>{{point.marker.position.lat().toFixed(6)}}</td>
<td>{{point.marker.position.lng().toFixed(6)}}</td>
<td><input type="number" min="0" max="90" ng-model="point.angle"> deg</td>
<td><button type="button" class="btn btn-danger" ng-click="removePoint($index)">Remove</button></td>
</tr>
</table>
<div style="margin: 6px">Click on the map to add a point</div><BR><BR>
<center>
<button type="button" class="btn btn-success" ng-disabled="places.length == 0" ng-click="getPasses()">Calculate Passes</button>
<span class='glyphicon-left glyphicon glyphicon-refresh spinning' ng-show='isGettingPasses'></span>
</center><BR>
<table border="1">
<tr>
<td><b>Date</b></td>
<td><b>Sat name</b></td>
<td><b>Start time</b></td>
<td><b>Max AoAs</b></td>
<td><b>End time</b></td>
<td><b>Total time of contact<b></td>
</tr>
<tr ng-repeat="pass in passes">
<td>{{getDate(pass.time_start)}}</td>
<td>{{pass.name}}</td>
<td>{{getTime(pass.time_start)}}</td>
<td style="white-space: nowrap;"><div ng-repeat="stat in pass.pointStats">
Point {{$index + 1}}: {{stat.maxElv.toFixed(0)}} deg @ {{getTime(stat.timeMaxElv)}}
</div></td>
<td>{{getTime(pass.time_end)}}</td>
<td>{{getDifferenceInMinutes(pass.time_end, pass.time_start)}}</td>
</tr>
</table><br>
<div id="footer">
<center>
Developed by Stephen Downward, callsign VA1QLE.<br>
<a href="https://git.scd31.com/laptopdude90/multipoint-satellite-tracker">Git repository</a>
</center>
</div>
</div>
<div id="map">
<ng-map center="39.82, -77.01" zoom="4"></ng-map>
</div>
</div>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
var app = angular.module('satTrackApp', ['ngMap']);
var socket = io();
app.controller('satTrack-ctrl', function($scope, NgMap) {
$scope.places = [];
$scope.passes = [];
$scope.isGettingPasses = false;
var elevator;
var numGetPasses = 0;
$scope.removePoint = function(id) {
$scope.places[id].marker.setMap(null);
$scope.places[id].marker = null;
$scope.places.splice(id, 1);
for(var i = 0; i < $scope.places.length; i++) {
$scope.places[i].marker.setLabel((i + 1).toString());
}
}
NgMap.getMap().then(function(map) {
google.maps.event.addListener(map, 'click', function(event) {
var marker = new google.maps.Marker({
position: event.latLng,
map: map,
label: ($scope.places.length + 1).toString(),
draggable: true
});
elevator = new google.maps.ElevationService;
google.maps.event.addListener(marker, 'dragend', function () {
$scope.$apply();
});
$scope.places.push({marker: marker, angle: 10});
$scope.$apply();
});
});
$scope.getPasses = function() {
$scope.isGettingPasses = true;
callsRemaining = $scope.places.length;
for(var i = 0; i < $scope.places.length; i++) {
let place = $scope.places[i];
elevator.getElevationForLocations({locations: [$scope.places[i].marker.position]}, function(results, status) {
if(status === 'OK' && results[0]) {
place.height = Math.max(results[0].elevation / 1000, 0);
callsRemaining--;
if(callsRemaining <= 0) sendCoords();
} else {
alert('Something went very wrong');
}
});
}
var sendCoords = function() {
var coords = $scope.places.map(function(a) {
return {
lat: a.marker.position.lat(),
lng: a.marker.position.lng(),
height: a.height,
angle: a.angle};
});
numGetPasses++;
socket.emit('getPasses', {coords: coords, checksum: numGetPasses});
}
}
$scope.getDate = function(date) {
//console.log(date);
return moment(date).format("MMM D");
}
$scope.getTime = function(date) {
return moment(date).format("HH:mm:ss");
}
$scope.getDifferenceInMinutes = function(a, b) {
var d = moment.duration(moment(a).diff(moment(b)));
return (d / 60000) + " minutes";
}
socket.on('passes', function(data) {
if(data.checksum == numGetPasses) {
$scope.passes = data.passes;
$scope.isGettingPasses = false;
$scope.$apply();
}
});
socket.on('satList', function(data) {
console.log(data);
});
});
function initMap() {
var f_coords = {lat: 44.648862, lng: -63.575320};
var s_coords = {lat: 51.165691, lng: 10.451526};
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 4,
center: f_coords
});
var f_marker = new google.maps.Marker({
position: f_coords,
map: map
});
var s_marker = new google.maps.Marker({
position: s_coords,
map: map
});
google.maps.event.addListener(map, 'mousemove', function (event) {
var lat = event.latLng.lat();
var long = event.latLng.lng();
console.log(latLongToCartesian({lat: lat, lng: long}));
});
var f = latLongToCartesian(f_coords);
f.ang = 10;
var s = latLongToCartesian(s_coords);
s.ang = 10;
slopes = getSlopes(f, s);
heatmap_data = [];
for(var lat = -90; lat < 90; lat++) {
for(var long = -180; long < 180; long++) {
var coordsToCheck = latLongToCartesian({lat: lat, lng: long}, 6000);
var y = coordsToCheck.x;
var x = coordsToCheck.y;
coordsToCheck.y = y;
coordsToCheck.x = x;
if(checkIfPointValid(coordsToCheck, slopes, f, s))
{
console.log("Lat:" + lat + " Long: " + long + " works");
heatmap_data.push(new google.maps.LatLng(lat, long));
}
}
}
var heatmap = new google.maps.visualization.HeatmapLayer({
data: heatmap_data,
dissipating: false,
map: map
});
}
var getSlopes = function(f, s) {
var fAng = f.ang * (Math.PI / 180);
var sAng = s.ang * (Math.PI / 180);
var fNorthernHemi = true;
var sNorthernHemi = true;
//Fix for southern hemisphere
if(f.y < 0) {
fAng *= -1;
fNorthernHemi = false;
}
if(s.y < 0) {
sAng *= -1;
sNorthernHemi = false;
console.log("boop" + s.y);
}
return {
h: Math.tan(fAng + Math.atan(-f.x/f.y)),
i: Math.tan(-sAng + Math.atan(-s.x/s.y)),
j: Math.tan(fAng + Math.atan(-f.z/f.y)),
k: Math.tan(-sAng + Math.atan(-s.z/s.y)),
fNHemi: fNorthernHemi,
sNHemi: sNorthernHemi
};
}
var checkIfPointValid = function(p, m, f, s) { //Point, slopes, first point, second point
var yh = m.h * (p.x - f.x) + f.y;
var yi = m.i * (p.x - s.x) + s.y;
var yj = m.j * (p.z - f.z) + f.y;
var yk = m.k * (p.z - s.z) + s.y;
if ((m.fNHemi && p.y >= yh && p.y >= yj) || (!m.fNHemi && p.y <= yh && p.y <= yj)) {
/*console.log(p.z + " " + s.z + " " + s.y + " " + m.k);
console.log(yk);
console.log(p.y);*/
if ((m.sNHemi && p.y >= yi && p.y >= yk) || (!m.sNHemi && p.y <= yi && p.y <= yk)) {
return true; //Yay!
}
}
return false;
}
var latLongToCartesian = function(point, height = 0) {
var earthRadius = 6371; //KM
var nRadius = earthRadius + height;
var nLat = (Math.PI/180) * point.lat;
var nLong = (Math.PI/180) * point.lng;
return {
/*x: nRadius * Math.cos(nLat) * Math.cos(nLong),
z: nRadius * Math.cos(nLat) * Math.sin(nLong),
y: nRadius * Math.sin(nLat)*/
x: nLat,
y: nLong,
z: height
};
}
This diff is collapsed.
This diff is collapsed.
var express = require('express'),
cluster = require('cluster'),
net = require('net'),
sio = require('socket.io'),
sio_redis = require('socket.io-redis');
var port = 3000,
num_processes = require('os').cpus().length;
if (cluster.isMaster) {
// This stores our workers. We need to keep them to be able to reference
// them based on source IP address. It's also useful for auto-restart,
// for example.
var workers = [];
// Helper function for spawning worker at index 'i'.
var spawn = function(i) {
workers[i] = cluster.fork();
// Optional: Restart worker on exit
workers[i].on('exit', function(code, signal) {
console.log('respawning worker', i);
spawn(i);
});
};
// Spawn workers.
for (var i = 0; i < num_processes; i++) {
spawn(i);
}
// Helper function for getting a worker index based on IP address.
// This is a hot path so it should be really fast. The way it works
// is by converting the IP address to a number by removing non numeric
// characters, then compressing it to the number of slots we have.
//
// Compared against "real" hashing (from the sticky-session code) and
// "real" IP number conversion, this function is on par in terms of
// worker index distribution only much faster.
var worker_index = function(ip, len) {
var s = '';
for (var i = 0, _len = ip.length; i < _len; i++) {
if (!isNaN(ip[i])) {
s += ip[i];
}
}
return Number(s) % len;
};
// Create the outside facing server listening on our port.
var server = net.createServer({ pauseOnConnect: true }, function(connection) {
// We received a connection and need to pass it to the appropriate
// worker. Get the worker for this connection's source IP and pass
// it the connection.
var worker = workers[worker_index(connection.remoteAddress, num_processes)];
worker.send('sticky-session:connection', connection);
}).listen(port);
} else {
// Note we don't use a port here because the master listens on it for us.
var server_local = require('./server.js');
server_local.startServer(0, function(server, io) {
// Here you might use middleware, attach routes, etc.
// Don't expose our internal server to the outside.
// Tell Socket.IO to use the redis adapter. By default, the redis
// server is assumed to be on localhost:6379. You don't have to
// specify them explicitly unless you want to change them.
io.adapter(sio_redis({ host: 'localhost', port: 6379 }));
// Here you might use Socket.IO middleware for authorization etc.
// Listen to messages sent from the master. Ignore everything else.
process.on('message', function(message, connection) {
if (message !== 'sticky-session:connection') {
return;
}
// Emulate a connection event on the server by emitting the
// event with the connection the master sent us.
server.emit('connection', connection);
connection.resume();
});
});
}
{
"name": "multipoint-satellite-tracker",
"version": "1.0.0",
"description": "Allows viable satellites to be found given multiple points on the Earth",
"main": "manager.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node manager.js"
},
"author": "Stephen Downward VA1QLE",
"license": "GPL-3.0",
"dependencies": {
"express": "^4.14.1",
"cluster": "^0.7.7",
"satellite.js": "^1.3.0",
"socket.io": "^1.7.2",
"socket.io-redis": "^4.0.0"
},
"devDependencies": {}
}
var fs = require('fs');
var satJS = require('satellite.js');
var express = require('express');
var app = new express();
var serv = require('http').Server(app);
var sio = require('socket.io');
//var io = require('socket.io')(serv, {});
var sio_redis = require('socket.io-redis');
var satellites = [];
exports.startServer = function(port, callback) {
updateSatelliteData(function() {
var server = app.listen(port, 'localhost');
var io = sio(server);
initalize(io);
callback(server, io);
console.log("Server started");
});
}
function initalize(io) {
//Web stuff
app.get('/', function(req, res) {
res.sendFile(__dirname + '/client/index.html');
});
app.use('/client', express.static(__dirname + '/client'));
io.sockets.on('connection', function(socket) {
//Send a list of satellites by name
var satNames = satellites.map(a => a.name);
//socket.emit('satList', satNames);
socket.on('getPasses', function(data) {
//The data returned from calcSats isn't fit for displaying, as it is arranged into groups by satallite. Below fixes that
//Make new array equal to all the old array.passes
var passes = calcSatellites(data.coords).map(function(a) {
//Add satellite names to each pass
a.passes.map(function(b) {
b.name = a.name;
return b;
});
return a.passes;
});
passes = flatten(passes);
//Sort by time_start
passes.sort(function(a,b){
return a.time_start - b.time_start;
});
//console.info(passes);
socket.emit('passes', {passes: passes, checksum: data.checksum});
});
});
//Update the satellite DB daily
//Update the satellite name DB weekly
}
function flatten(arr) {
return arr.reduce(function (flat, toFlatten) {
return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
}, []);
}
function calcSatellites(ground_coords) {
//Convert coords to radians
var ground_coords_rad = [];
for(var i = 0; i < ground_coords.length; i++) { //Coords
var radian_coords = {
latitude: ground_coords[i].lat * Math.PI / 180,
longitude: ground_coords[i].lng * Math.PI / 180,
height: ground_coords[i].height};
ground_coords_rad.push(radian_coords);
}
var viableSats = [];
var currentTimestamp = new Date();
for(var i = 0; i < satellites.length; i++) { //Satellites
if(satellites[i].satrec.error === 0) {
var passes = [];
var time_start = 0;
var pointStats = [];
for(var secondsFromNow = 0; secondsFromNow < 604800; secondsFromNow += 30) { //Up to a week in the future, in 30 second increments
var timeToProgressTo = new Date(currentTimestamp.getTime() + (secondsFromNow * 1000));
//console.log(satellites[i].satrec.error);
var viableTime = true;
for(var j = 0; j < ground_coords_rad.length; j++) { //Coordinates
var g_coords = ground_coords_rad[j];
var posAndVel = satJS.satellite.propagate(satellites[i].satrec, timeToProgressTo);
if(posAndVel.position != false && posAndVel.position != null) {
var lookAngles = satJS.satellite.ecfToLookAngles(g_coords, satJS.satellite.eciToEcf(posAndVel.position, satJS.satellite.gstimeFromDate(timeToProgressTo)));
var elevationDeg = lookAngles.elevation * 180 / Math.PI;
//Define pointStats[j]
if(!pointStats[j]) pointStats[j] = {maxElv: 0, timeMaxElv: 0};
if(elevationDeg > pointStats[j].maxElv && elevationDeg > ground_coords[j].angle) { //Has to be during a pass
pointStats[j].maxElv = elevationDeg;
pointStats[j].timeMaxElv = timeToProgressTo;
}
if(elevationDeg < ground_coords[j].angle) {
viableTime = false;
if(time_start != 0) {
//Push pass to array and reset for next pass
passes.push({
time_start: time_start,
pointStats: pointStats,
time_end: timeToProgressTo
});
time_start = 0;
pointStats = [];
}
break;
}
}
}
if(viableTime) {
if(time_start == 0) time_start = timeToProgressTo;
}