Initial commit
This commit is contained in:
32
frontend/Dockerfile
Normal file
32
frontend/Dockerfile
Normal 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
111
frontend/package-lock.json
generated
Normal 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
25
frontend/package.json
Normal 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
103
frontend/src/index.html
Normal 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
109
frontend/src/main.js
Normal 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);
|
||||
1
frontend/src/privacy-policy.html
Normal file
1
frontend/src/privacy-policy.html
Normal file
@@ -0,0 +1 @@
|
||||
<!-- Fill this privacy policy. -->
|
||||
73
frontend/src/style.css
Normal file
73
frontend/src/style.css
Normal 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;
|
||||
}
|
||||
91
frontend/src/table.handlebars
Normal file
91
frontend/src/table.handlebars
Normal 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}}
|
||||
Reference in New Issue
Block a user