Initial commit

This commit is contained in:
2024-03-12 22:28:05 +01:00
commit 7711bcc220
40 changed files with 5137 additions and 0 deletions

32
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
FROM node:alpine AS install
WORKDIR /app
COPY package-lock.json .
COPY package.json .
RUN npm i
FROM install AS compile
WORKDIR /app
COPY src src
RUN mkdir dist
RUN npx handlebars src/table.handlebars -f dist/table.handlebars.compiled.js
COPY --from=install /app/node_modules/handlebars/dist/handlebars.min.js dist
COPY --from=install /app/node_modules/bootstrap/dist/js/bootstrap.min.js dist
COPY --from=install /app/node_modules/bootstrap/dist/js/bootstrap.min.js.map dist
COPY --from=install /app/node_modules/bootstrap/dist/css/bootstrap.min.css dist
COPY --from=install /app/node_modules/bootstrap/dist/css/bootstrap.min.css.map dist
COPY src/index.html dist
COPY src/privacy-policy.html dist
COPY src/main.js dist
COPY src/style.css dist
FROM nginx:alpine
COPY --from=compile /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]

111
frontend/package-lock.json generated Normal file
View File

@@ -0,0 +1,111 @@
{
"name": "above_me",
"version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "above_me",
"version": "0.1.4",
"license": "MIT",
"dependencies": {
"bootstrap": "^5.3.8",
"handlebars": "^4.7.8"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/bootstrap": {
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"license": "MIT"
}
}
}

25
frontend/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "above_me",
"version": "0.1.4",
"description": "This project contains the frontend for above_me, a service that lets you see what aircraft are currently flying above you.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"copy-dependencies": "rm -rf dist && rsync -av --exclude='*.handlebars' src/ dist/ && cp node_modules/handlebars/dist/handlebars.min.js dist/ && cp node_modules/bootstrap/dist/js/bootstrap.min.js dist && cp node_modules/bootstrap/dist/js/bootstrap.min.js.map dist && cp node_modules/bootstrap/dist/css/bootstrap.min.css dist && cp node_modules/bootstrap/dist/css/bootstrap.min.css.map dist",
"compile": "handlebars src/table.handlebars -f dist/table.handlebars.compiled.js",
"dev": "npm run copy-dependencies && npm run compile && cd dist && python3 -m http.server 8080"
},
"keywords": [
"ogn",
"openglidernet",
"open-glider-net",
"aircraft",
"glider"
],
"author": "Laika Schmidt",
"license": "MIT",
"dependencies": {
"bootstrap": "^5.3.8",
"handlebars": "^4.7.8"
}
}

103
frontend/src/index.html Normal file
View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>above_me</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="stylesheet" href="bootstrap.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">
above_me
</a>
<div class="links">
<a href="privacy-policy.html">
Privacy policy
</a>
</div>
</div>
</nav>
<div class="container">
<h3>See what's flying right above you!</h1>
<div class="button-container">
<button type="button" class="btn btn-primary btn-lg" onclick="onClickWhatsAboveMe()"
aria-describedby="whats-above-me-hint">What's above
me?</button>
<div id="whats-above-me-hint" class="form-text">
By clicking this button you accept our <a href="privacy-policy.html">privacy policy</a> and also
that
your current position is sent to the webserver.
</div>
</div>
<div class="alert alert-danger status-message" role="alert" id="no-position-available">
Error. Position for your device could not be loaded. Please check if you gave permission to this site to
access your position.
</div>
<div class="alert alert-danger status-message" role="alert" id="http-error">
Error. Could not fetch data. Open the developer console for more information.
</div>
<div class="alert alert-info status-message" role="alert" id="loading-position">
Loading position...
</div>
<div id="table-container" class="overflow-auto"></div>
<p>
Either click <i>What's above me?</i> to automatically fetch your location
or enter specific coordinates and range and see whats flying above you right now.
</p>
<p>
This site works with data provided by the
<a href="http://wiki.glidernet.org/" target="_blank">Open Glider Network</a>.
(Thank you for your open data & APIs! 😊)
</p>
<form>
<div class="mb-3">
<label for="latitude" class="form-label">Latitude</label>
<input class="form-control" type="number" name="latitude" id="latitude" placeholder="48.858222"
min="-90" max="90" step=".000000001">
</div>
<div class="mb-3">
<label for="longitude" class="form-label">Longitude</label>
<input class="form-control" type="number" name="longitude" id="longitude" placeholder="2.2945"
min="-180" max="180" step=".000000001">
</div>
<div class="mb-3">
<label for="range" class="form-label">Range</label>
<input class="form-control" type="number" name="range" id="range" min="1" max="20" value="20"
placeholder="1 - 20 km" aria-describedby="range-help" step="1">
<div id="range-help" class="form-text">
Range (in <i>km</i>) around given coordinates that should be filtered for.
</div>
</div>
<div class="mb-3 button-container">
<button type="submit" class="btn btn-primary" aria-describedby="submit-hint">Submit</button>
<div id="submit-hint" class="form-text">
By clicking this button you accept our <a href="privacy-policy.html">privacy policy</a> and also
that the given position is sent to the webserver.
</div>
</div>
</form>
</div>
<script src="bootstrap.min.js"></script>
<script src="handlebars.min.js"></script>
<script src="table.handlebars.compiled.js"></script>
<script src="main.js"></script>
</body>
</html>

109
frontend/src/main.js Normal file
View File

@@ -0,0 +1,109 @@
function submit(event) {
document.getElementById('http-error').style.display = 'none';
if (event) {
event.preventDefault();
}
const latitude = document.querySelector('#latitude').value;
const longitude = document.querySelector('#longitude').value;
const range = document.querySelector('#range').value;
if (!latitude || !longitude || !range) {
return;
}
const url = window.location.origin + `/r/${latitude}/${longitude}/${range}`;
const currentTimestamp = Math.round(Date.now() / 1000);
fetch(url)
.then(response => response.json())
.then(response => response.states)
.then(states =>
states.map(s => ({
...s,
speed: s.speed?.toFixed(0),
vertical_speed: s.vertical_speed?.toFixed(1),
altitude: s.altitude?.toFixed(0),
turn_rate: s.turn_rate?.toFixed(1),
course: s.course?.toFixed(0),
position: {
...s.position,
longitude_text: formatCoordinateValue(s.position.longitude, 'E', 'W'),
latitude_text: formatCoordinateValue(s.position.latitude, 'N', 'S'),
},
time_diff: formatTimeDiff(s.time_stamp, currentTimestamp),
distance: s.distance.toFixed(0),
})
))
.then(states =>
Handlebars.templates.table({ states })
)
.then(html => {
document.getElementById('table-container').innerHTML = html;
})
.catch(error => {
document.getElementById('http-error').style.display = 'inherit';
console.error(error);
})
}
function formatCoordinateValue(latitude, directionPositive, directionNegative) {
const degrees = Math.floor(latitude);
const minutes = (latitude - degrees) * 60;
const seconds = (minutes % 1) * 60;
const direction = latitude > 0 ? directionPositive : directionNegative;
return `${degrees.toString().padStart(3, '0')}°${minutes.toFixed(0)}'${seconds.toFixed(0)}" ${direction}`
}
function formatTimeDiff(timestamp1, timestamp2) {
const diff = Math.abs(timestamp1 - timestamp2);
if (diff === 0) {
return 'now';
}
if (diff < 60) {
return `${diff} s`;
}
return `${Math.round(diff / 60)} min`;
}
function onClickWhatsAboveMe() {
document.querySelector('#loading-position').style.display = 'inherit';
navigator.geolocation.getCurrentPosition(
(position) => {
document.querySelector('#latitude').value = Math.round(position.coords.latitude * 1_000_000_000) / 1_000_000_000;
document.querySelector('#longitude').value = Math.round(position.coords.longitude * 1_000_000_000) / 1_000_000_000;
document.querySelector('#no-position-available').style.display = 'none';
document.querySelector('#loading-position').style.display = 'none';
submit();
},
() => {
document.querySelector('#no-position-available').style.display = 'inherit';
document.querySelector('#loading-position').style.display = 'none';
});
}
(function init() {
const urlParams = new URLSearchParams(window.location.search);
for (let param of ['longitude', 'latitude', 'range']) {
if (!urlParams.has(param)) {
continue;
}
let value = parseFloat(urlParams.get(param));
if (!value || isNaN(value)) {
continue;
}
document.querySelector(`#${param}`).value = value;
}
})();
document.querySelector('form').addEventListener('submit', submit);

View File

@@ -0,0 +1 @@
<!-- Fill this privacy policy. -->

73
frontend/src/style.css Normal file
View File

@@ -0,0 +1,73 @@
nav {
margin-bottom: 3%;
}
.button-container {
margin: 20% 0;
display: flex;
justify-content: center;
flex-direction: column;
}
form {
margin-top: 12.5%;
}
form .button-container {
margin-top: 12.5%;
}
@media (min-width: 576px) {
.button-container {
margin: 5% 0;
}
form {
margin-top: 5%;
}
form .button-container {
margin-top: 5%;
}
}
#table-container {
margin: 5% 0;
white-space: nowrap;
}
.status-message {
display: none;
}
h3 {
text-align: center;
}
.form-text {
text-align: center;
}
.links * {
padding-left: 10px;
color: white;
text-decoration: none;
}
.links *:hover {
text-decoration: underline;
}
.variable-value {
text-align: right;
}
.units * {
font-weight: normal;
text-align: right;
font-style: italic;
}
.units * b {
font-weight: bold;
}

View File

@@ -0,0 +1,91 @@
{{#if states}}
<table class="table table-striped table-bordered table-dresponsive">
<thead>
<tr class="table-dark">
<th scope="col">Id</th>
<th scope="col">Distance</th>
<th scope="col">Altitude</th>
<th scope="col">Speed</th>
<th scope="col">Course</th>
<th scope="col">Vertical speed</th>
<th scope="col">Turn rate</th>
<th scope="col">Position</th>
<th scope="col">Last status</th>
</tr>
<tr class="table-primary units">
<th scope="col">
<b>Registration</b> (call sign)<br>
Model / type
</th>
<th scope="col">km</th>
<th scope="col">m</th>
<th scope="col">km/h</th>
<th scope="col">°</th>
<th scope="col">m/s</th>
<th scope="col">turns/min</th>
<th scope="col">
Link to map<br>
(<i><a href="https://live.glidernet.org/">live.glidernet.org</a></i>)
</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{{#each states}}
<tr>
<td class="variable-value">
{{#if aircraft.registration}}
<b>{{aircraft.registration}}</b>
{{#if aircraft.call_sign}}({{aircraft.call_sign}}){{/if}}
<br>{{aircraft.model}}
{{/if}}
{{#unless aircraft.registration}}
<i>unknown</i>
{{#if aircraft.model}}<br>{{/if}}
{{aircraft.model}}
{{/unless}}
</td>
<td class="variable-value">
{{#if distance}}{{distance}}{{/if}}
{{#unless distance}}-{{/unless}}
</td>
<td class="variable-value">
{{#if altitude}}{{altitude}}{{/if}}
{{#unless altitude}}-{{/unless}}
</td>
<td class="variable-value">
{{#if speed}}{{speed}}{{/if}}
{{#unless speed}}-{{/unless}}
</td>
<td class="variable-value">
{{#if course}}{{course}}{{/if}}
{{#unless course}}-{{/unless}}
</td>
<td class="variable-value">
{{#if vertical_speed}}{{vertical_speed}}{{/if}}
{{#unless vertical_speed}}-{{/unless}}
</td>
<td class="variable-value">
{{#if turn_rate}}{{turn_rate}}{{/if}}
{{#unless turn_rate}}-{{/unless}}
</td>
<td class="variable-value">
<a href="https://live.glidernet.org/#c={{position.latitude}},{{position.longitude}}&z=15&s=1"
target="_blank">
{{position.latitude_text}}<br>
{{position.longitude_text}}
</a>
</td>
<td class="variable-value">
{{time_diff}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
{{#unless states}}
<div class="alert alert-secondary" role="alert">
Nothing's above you :(
</div>
{{/unless}}