New app: flightdash

pull/3183/head
Flaparoo 2023-11-28 17:54:28 +08:00
parent ed2d6e8794
commit fb9ebccb41
10 changed files with 1157 additions and 0 deletions

View File

@ -0,0 +1 @@
1.00: initial release

76
apps/flightdash/README.md Normal file
View File

@ -0,0 +1,76 @@
# Flight Dashboard
Shows basic flight and navigation instruments.
![](screenshot.png)
Basic flight data includes:
- Ground speed
- Track
- Altimeter
- VSI
- Local time
You can also set a destination to get nav guidance:
- Distance from destination
- Bearing to destination
- Estimated Time En-route (minutes and seconds)
- Estimated Time of Arrival (in UTC)
The speed/distance and altitude units are configurable.
Altitude data can be derived from GPS or the Bangle's barometer.
## DISCLAIMER
Remember to Aviate - Navigate - Communicate! Do NOT get distracted by your
gadgets, keep your eyes looking outside and do NOT rely on this app for actual
navigation!
## Usage
After installing the app, use the "interface" page (floppy disk icon) in the
App Loader to filter and upload a list of airports (to be used as navigation
destinations). Due to memory constraints, only up to about 500 airports can be
stored on the Bangle itself (recommended is around 100 - 150 airports max.).
Then, on the Bangle, access the Flight-Dash settings, either through the
Settings app (Settings -> Apps -> Flight-Dash) or a tap anywhere in the
Flight-Dash app itself. The following settings are available:
- **Nav Dest.**: Choose the navigation destination:
- Nearest airports (from the uploaded list)
- Search the uploaded list of airports
- User waypoints (which can be set/edited through the settings)
- Nearest airports (queried online through AVWX - requires Internet connection at the time)
- **Speed** and **Altitude**: Set the preferred units of measurements.
- **Use Baro**: If enabled, altitude information is derived from the Bangle's barometer (instead of using GPS altitude).
If the barometer is used for altitude information, the current QNH value is
also displayed. It can be adjusted by swiping up/down in the app.
To query the nearest airports online through AVWX, you have to install - and
configure - the [avwx](?id=avwx) module.
The app requires a text input method (to set user waypoint names, and search
for airports), and if not already installed will automatically install the
default "textinput" app as a dependency.
## Hint
Under the bearing "band", the current nav destination is displayed. Next to
that, you'll also find the cardinal direction you are approaching **from**.
This can be useful for inbound radio calls. Together with the distance, the
current altitude and the ETA, you have all the information required to make
radio calls like a pro!
## Author
Flaparoo [github](https://github.com/flaparoo)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4A/AHcCkAX/C/4X9kUiC/4XcgczmcwRSArBkYWBAAMyA4KUMC4QWDAAIXOAAUziMTmMRmZdRmQXDkZhIHQJ1IAAYXGBgoNDgQJFLoQhFDQ84wQFDlGDBwxBInGIDAUoxAXFJosDOIIXDAAgXCPoJkGBAKfBmc6C4ujBIINBiYXIEIMK1AWDxWgHoQXMgGqC4eqKoYXHL4QFChQYC1QuBEwbcHZo7hHBpYA/AH4A/AH4"))

View File

@ -0,0 +1,527 @@
/*
* Flight Dashboard - Bangle.js
*/
const COLOUR_BLACK = 0x0000; // same as: g.setColor(0, 0, 0)
const COLOUR_WHITE = 0xffff; // same as: g.setColor(1, 1, 1)
const COLOUR_GREEN = 0x07e0; // same as: g.setColor(0, 1, 0)
const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0)
const COLOUR_MAGENTA = 0xf81f; // same as: g.setColor(1, 0, 1)
const COLOUR_CYAN = 0x07ff; // same as: g.setColor(0, 1, 1)
const COLOUR_LIGHT_BLUE = 0x841f; // same as: g.setColor(0.5, 0.5, 1)
const APP_NAME = 'flightdash';
const horizontalCenter = g.getWidth() / 2;
const verticalCenter = g.getHeight() / 2;
const dataFontHeight = 22;
const secondaryFontHeight = 18;
const labelFontHeight = 12;
//globals
var settings = {};
var updateInterval;
var speed = '-'; var speedPrev = -1;
var track = '-'; var trackPrev = -1;
var lat = 0; var lon = 0;
var distance = '-'; var distancePrev = -1;
var bearing = '-'; var bearingPrev = -1;
var relativeBearing = 0; var relativeBearingPrev = -1;
var fromCardinal = '-';
var ETAdate = new Date();
var ETA = '-'; var ETAPrev = '';
var QNH = Math.round(Bangle.getOptions().seaLevelPressure); var QNHPrev = -1;
var altitude = '-'; var altitudePrev = -1;
var VSI = '-'; var VSIPrev = -1;
var VSIraw = 0;
var VSIprevTimestamp = Date.now();
var VSIprevAltitude;
var VSIsamples = 0; var VSIsamplesCount = 0;
var speedUnit = 'N/A';
var distanceUnit = 'N/A';
var altUnit = 'N/A';
// date object to time string in format (HH:MM[:SS])
function timeStr(date, seconds) {
let timeStr = date.getHours().toString();
if (timeStr.length == 1) timeStr = '0' + timeStr;
let minutes = date.getMinutes().toString();
if (minutes.length == 1) minutes = '0' + minutes;
timeStr += ':' + minutes;
if (seconds) {
let seconds = date.getSeconds().toString();
if (seconds.length == 1) seconds = '0' + seconds;
timeStr += ':' + seconds;
}
return timeStr;
}
// add thousands separator to number
function addThousandSeparator(n) {
let s = n.toString();
if (s.length > 3) {
return s.substr(0, s.length - 3) + ',' + s.substr(s.length - 3, 3);
} else {
return s;
}
}
// update VSI
function updateVSI(alt) {
VSIsamples += alt; VSIsamplesCount += 1;
let VSInewTimestamp = Date.now();
if (VSIprevTimestamp + 1000 <= VSInewTimestamp) { // update VSI every 1 second
let VSInewAltitude = VSIsamples / VSIsamplesCount;
if (VSIprevAltitude) {
let VSIinterval = (VSInewTimestamp - VSIprevTimestamp) / 1000;
VSIraw = (VSInewAltitude - VSIprevAltitude) * 60 / VSIinterval; // extrapolate to change / minute
}
VSIprevTimestamp = VSInewTimestamp;
VSIprevAltitude = VSInewAltitude;
VSIsamples = 0; VSIsamplesCount = 0;
}
VSI = Math.floor(VSIraw / 10) * 10; // "smooth" VSI value
if (settings.altimeterUnits == 0) { // Feet
VSI = Math.round(VSI * 3.28084);
} // nothing else required since VSI is already in meters ("smoothed")
if (VSI > 9999) VSI = 9999;
else if (VSI < -9999) VSI = -9999;
}
// update GPS-derived information
function updateGPS(fix) {
if (!('fix' in fix) || fix.fix == 0 || fix.satellites < 4) return;
speed = 'N/A';
if (settings.speedUnits == 0) { // Knots
speed = Math.round(fix.speed * 0.539957);
} else if (settings.speedUnits == 1) { // km/h
speed = Math.round(fix.speed);
} else if (settings.speedUnits == 2) { // MPH
speed = Math.round(fix.speed * 0.621371);
}
if (speed > 9999) speed = 9999;
if (! settings.useBaro) { // use GPS altitude
altitude = 'N/A';
if (settings.altimeterUnits == 0) { // Feet
altitude = Math.round(fix.alt * 3.28084);
} else if (settings.altimeterUnits == 1) { // Meters
altitude = Math.round(fix.alt);
}
if (altitude > 99999) altitude = 99999;
updateVSI(fix.alt);
}
track = Math.round(fix.course);
if (isNaN(track)) track = '-';
else if (track < 10) track = '00'+track;
else if (track < 100) track = '0'+track;
lat = fix.lat;
lon = fix.lon;
// calculation from https://www.movable-type.co.uk/scripts/latlong.html
const latRad1 = lat * Math.PI/180;
const latRad2 = settings.destLat * Math.PI/180;
const lonRad1 = lon * Math.PI/180;
const lonRad2 = settings.destLon * Math.PI/180;
// distance (using "Equirectangular approximation")
let x = (lonRad2 - lonRad1) * Math.cos((latRad1 + latRad2) / 2);
let y = (latRad2 - latRad1);
let distanceNumber = Math.sqrt(x*x + y*y) * 6371; // in km - 6371 = mean Earth radius
if (settings.speedUnits == 0) { // NM
distanceNumber = distanceNumber * 0.539957;
} else if (settings.speedUnits == 2) { // miles
distanceNumber = distanceNumber * 0.621371;
}
if (distanceNumber > 99.9) {
distance = '>100';
} else {
distance = (Math.round(distanceNumber * 10) / 10).toString();
if (! distance.includes('.'))
distance += '.0';
}
// bearing
y = Math.sin(lonRad2 - lonRad1) * Math.cos(latRad2);
x = Math.cos(latRad1) * Math.sin(latRad2) -
Math.sin(latRad1) * Math.cos(latRad2) * Math.cos(lonRad2 - lonRad1);
let nonNormalisedBearing = Math.atan2(y, x);
bearing = Math.round((nonNormalisedBearing * 180 / Math.PI + 360) % 360);
if (bearing > 337 || bearing < 23) {
fromCardinal = 'S';
} else if (bearing < 68) {
fromCardinal = 'SW';
} else if (bearing < 113) {
fromCardinal = 'W';
} else if (bearing < 158) {
fromCardinal = 'NW';
} else if (bearing < 203) {
fromCardinal = 'N';
} else if (bearing < 248) {
fromCardinal = 'NE';
} else if (bearing < 293) {
fromCardinal = 'E';
} else{
fromCardinal = 'SE';
}
if (bearing < 10) bearing = '00'+bearing;
else if (bearing < 100) bearing = '0'+bearing;
relativeBearing = parseInt(bearing) - parseInt(track);
if (isNaN(relativeBearing)) relativeBearing = 0;
if (relativeBearing > 180) relativeBearing -= 360;
else if (relativeBearing < -180) relativeBearing += 360;
// ETA
if (speed) {
let ETE = distanceNumber * 3600 / speed;
let now = new Date();
ETAdate = new Date(now + (now.getTimezoneOffset() * 1000 * 60) + ETE*1000);
if (ETE < 86400) {
ETA = timeStr(ETAdate, false);
} else {
ETA = '>24h';
}
} else {
ETAdate = new Date();
ETA = '-';
}
}
// update barometric information
function updatePressure(e) {
altitude = 'N/A';
if (settings.altimeterUnits == 0) { // Feet
altitude = Math.round(e.altitude * 3.28084);
} else if (settings.altimeterUnits == 1) { // Meters
altitude = Math.round(e.altitude); // altitude is given in meters
}
if (altitude > 99999) altitude = 99999;
updateVSI(e.altitude);
}
// (re-)draw all read-outs
function draw(initial) {
g.setBgColor(COLOUR_BLACK);
// speed
if (speed != speedPrev || initial) {
g.setFontAlign(-1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_GREEN);
g.clearRect(0, 0, 55, dataFontHeight);
g.drawString(speed.toString(), 0, 0, false);
if (initial) {
g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(speedUnit, 0, dataFontHeight, false);
}
speedPrev = speed;
}
// distance
if (distance != distancePrev || initial) {
g.setFontAlign(1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE);
g.clearRect(g.getWidth() - 58, 0, g.getWidth(), dataFontHeight);
g.drawString(distance, g.getWidth(), 0, false);
if (initial) {
g.setFontAlign(1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(distanceUnit, g.getWidth(), dataFontHeight, false);
}
distancePrev = distance;
}
// track (+ static track/bearing content)
let trackY = 18;
let destInfoY = trackY + 53;
if (track != trackPrev || initial) {
g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE);
g.clearRect(horizontalCenter - 29, trackY, horizontalCenter + 28, trackY + dataFontHeight);
g.drawString(track.toString() + "\xB0", horizontalCenter + 3, trackY, false);
if (initial) {
let y = trackY + dataFontHeight + 1;
g.setColor(COLOUR_YELLOW);
g.drawRect(horizontalCenter - 30, trackY - 3, horizontalCenter + 29, y);
g.drawLine(0, y, g.getWidth(), y);
y += dataFontHeight + 5;
g.drawLine(0, y, g.getWidth(), y);
g.setFontAlign(1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA);
g.drawString(settings.destID, horizontalCenter, destInfoY, false);
}
trackPrev = track;
}
// bearing
if (bearing != bearingPrev || relativeBearing != relativeBearingPrev || initial) {
let bearingY = trackY + 27;
g.clearRect(0, bearingY, g.getWidth(), bearingY + dataFontHeight);
g.setColor(COLOUR_YELLOW);
for (let i = Math.floor(relativeBearing * 2.5) % 25; i <= g.getWidth(); i += 25) {
g.drawLine(i, bearingY + 3, i, bearingY + 16);
}
let bearingX = horizontalCenter + relativeBearing * 2.5;
if (bearingX > g.getWidth() - 26) bearingX = g.getWidth() - 26;
else if (bearingX < 26) bearingX = 26;
g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_MAGENTA);
g.drawString(bearing.toString() + "\xB0", bearingX + 3, bearingY, false);
g.clearRect(horizontalCenter + 42, destInfoY, horizontalCenter + 69, destInfoY + secondaryFontHeight);
g.setFontAlign(-1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA);
g.drawString(fromCardinal, horizontalCenter + 42, destInfoY, false);
if (initial) {
g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(' from', horizontalCenter, destInfoY, false);
}
bearingPrev = bearing;
relativeBearingPrev = relativeBearing;
}
let row3y = g.getHeight() - 48;
// QNH
if (settings.useBaro) {
if (QNH != QNHPrev || initial) {
let QNHy = row3y - secondaryFontHeight - 2;
g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
g.clearRect(horizontalCenter - 29, QNHy - secondaryFontHeight, horizontalCenter + 22, QNHy);
g.drawString(QNH.toString(), horizontalCenter - 3, QNHy, false);
if (initial) {
g.setFontAlign(0, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString('QNH', horizontalCenter - 3, QNHy, false);
}
QNHPrev = QNH;
}
}
// VSI
if (VSI != VSIPrev || initial) {
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
g.clearRect(0, row3y - secondaryFontHeight, 51, row3y);
g.drawString(VSI.toString(), 0, row3y, false);
if (initial) {
g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(altUnit + '/min', 0, row3y - secondaryFontHeight, false);
}
let VSIarrowX = 6;
let VSIarrowY = row3y - 42;
g.clearRect(VSIarrowX - 7, VSIarrowY - 10, VSIarrowX + 6, VSIarrowY + 10);
g.setColor(COLOUR_WHITE);
if (VSIraw > 30) { // climbing
g.fillRect(VSIarrowX - 1, VSIarrowY, VSIarrowX + 1, VSIarrowY + 10);
g.fillPoly([ VSIarrowX , VSIarrowY - 11,
VSIarrowX + 7, VSIarrowY,
VSIarrowX - 7, VSIarrowY]);
} else if (VSIraw < -30) { // descending
g.fillRect(VSIarrowX - 1, VSIarrowY - 10, VSIarrowX + 1, VSIarrowY);
g.fillPoly([ VSIarrowX , VSIarrowY + 11,
VSIarrowX + 7, VSIarrowY,
VSIarrowX - 7, VSIarrowY ]);
}
}
// altitude
if (altitude != altitudePrev || initial) {
g.setFontAlign(1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
g.clearRect(g.getWidth() - 65, row3y - secondaryFontHeight, g.getWidth(), row3y);
g.drawString(addThousandSeparator(altitude), g.getWidth(), row3y, false);
if (initial) {
g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(altUnit, g.getWidth(), row3y - secondaryFontHeight, false);
}
altitudePrev = altitude;
}
// time
let now = new Date();
let nowUTC = new Date(now + (now.getTimezoneOffset() * 1000 * 60));
g.setFontAlign(-1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_LIGHT_BLUE);
let timeStrMetrics = g.stringMetrics(timeStr(now, false));
g.drawString(timeStr(now, false), 0, g.getHeight(), true);
let seconds = now.getSeconds().toString();
if (seconds.length == 1) seconds = '0' + seconds;
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight);
g.drawString(seconds, timeStrMetrics.width + 2, g.getHeight() - 1, true);
if (initial) {
g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString('LOCAL', 0, g.getHeight() - dataFontHeight, false);
}
// ETE
let ETEy = g.getHeight() - dataFontHeight;
let ETE = '-';
if (ETA != '-') {
let ETEseconds = Math.floor((ETAdate - nowUTC) / 1000);
if (ETEseconds < 0) ETEseconds = 0;
ETE = ETEseconds % 60;
if (ETE < 10) ETE = '0' + ETE;
ETE = Math.floor(ETEseconds / 60) + ':' + ETE;
if (ETE.length > 6) ETE = '>999m';
}
g.clearRect(horizontalCenter - 35, ETEy - secondaryFontHeight, horizontalCenter + 29, ETEy);
g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
g.drawString(ETE, horizontalCenter - 3, ETEy, false);
if (initial) {
g.setFontAlign(0, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString('ETE', horizontalCenter - 3, ETEy - secondaryFontHeight, false);
}
// ETA
if (ETA != ETAPrev || initial) {
g.clearRect(g.getWidth() - 63, g.getHeight() - dataFontHeight, g.getWidth(), g.getHeight());
g.setFontAlign(1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE);
g.drawString(ETA, g.getWidth(), g.getHeight(), false);
if (initial) {
g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString('UTC ETA', g.getWidth(), g.getHeight() - dataFontHeight, false);
}
ETAPrev = ETA;
}
}
function handleSwipes(directionLR, directionUD) {
if (directionUD == -1) { // up -> increase QNH
QNH = Math.round(Bangle.getOptions().seaLevelPressure);
QNH++;
Bangle.setOptions({'seaLevelPressure': QNH});
} else if (directionUD == 1) { // down -> decrease QNH
QNH = Math.round(Bangle.getOptions().seaLevelPressure);
QNH--;
Bangle.setOptions({'seaLevelPressure': QNH});
}
}
function handleTouch(button, xy) {
if ('handled' in xy && xy.handled) return;
Bangle.removeListener('touch', handleTouch);
if (settings.useBaro) {
Bangle.removeListener('swipe', handleSwipes);
}
// any touch -> show settings
clearInterval(updateTimeInterval);
Bangle.setGPSPower(false, APP_NAME);
if (settings.useBaro)
Bangle.setBarometerPower(false, APP_NAME);
eval(require("Storage").read(APP_NAME+'.settings.js'))( () => {
E.showMenu();
// "clear" values potentially affected by a settings change
speed = '-'; distance = '-';
altitude = '-'; VSI = '-';
// re-launch
start();
});
}
/*
* main
*/
function start() {
// read in the settings
settings = Object.assign({
useBaro: false,
speedUnits: 0, // KTS
altimeterUnits: 0, // FT
destID: 'KOSH',
destLat: 43.9844,
destLon: -88.5570,
}, require('Storage').readJSON(APP_NAME+'.json', true) || {});
// set units
if (settings.speedUnits == 0) { // Knots
speedUnit = 'KTS';
distanceUnit = 'NM';
} else if (settings.speedUnits == 1) { // km/h
speedUnit = 'KPH';
distanceUnit = 'KM';
} else if (settings.speedUnits == 2) { // MPH
speedUnit = 'MPH';
distanceUnit = 'SM';
}
if (settings.altimeterUnits == 0) { // Feet
altUnit = 'FT';
} else if (settings.altimeterUnits == 1) { // Meters
altUnit = 'M';
}
// initialise
g.reset();
g.setBgColor(COLOUR_BLACK);
g.clear();
// draw incl. static components
draw(true);
// enable timeout/interval and sensors
setTimeout(function() {
draw();
updateTimeInterval = setInterval(draw, 1000);
}, 1000 - (Date.now() % 1000));
Bangle.setGPSPower(true, APP_NAME);
Bangle.on('GPS', updateGPS);
if (settings.useBaro) {
Bangle.setBarometerPower(true, APP_NAME);
Bangle.on('pressure', updatePressure);
}
// handle interaction
if (settings.useBaro) {
Bangle.on('swipe', handleSwipes);
}
Bangle.on('touch', handleTouch);
setWatch(e => { Bangle.showClock(); }, BTN1); // exit on button press
}
start();
/*
// TMP for testing:
//settings.speedUnits = 1;
//settings.altimeterUnits = 1;
QNH = 1013;
updateGPS({"fix":1,"speed":228,"alt":3763,"course":329,"lat":36.0182,"lon":-75.6713});
updatePressure({"altitude":3700});
*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

View File

@ -0,0 +1,344 @@
(function(back) {
const APP_NAME = 'flightdash';
const FILE = APP_NAME+'.json';
// if the avwx module is available, include an extra menu item to query nearest airports via AVWX
var avwx;
try {
avwx = require('avwx');
} catch (error) {
// avwx module not installed
}
// Load settings
var settings = Object.assign({
useBaro: false,
speedUnits: 0, // KTS
altimeterUnits: 0, // FT
destID: 'KOSH',
destLat: 43.9844,
destLon: -88.5570,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
var airports; // cache list of airports
function readAirportsList(empty_cb) {
if (airports) { // airport list has already been read in
return true;
}
airports = require('Storage').readJSON(APP_NAME+'.airports.json', true);
if (! airports) {
E.showPrompt('No airports stored - download from the Bangle Apps Loader!',
{title: 'Flight-Dash', buttons: {OK: true} }).then((v) => {
empty_cb();
});
return false;
}
return true;
}
// use GPS fix
var afterGPSfixMenu = 'destNearest';
function getLatLon(fix) {
if (!('fix' in fix) || fix.fix == 0 || fix.satellites < 4) return;
Bangle.setGPSPower(false, APP_NAME+'-settings');
Bangle.removeListener('GPS', getLatLon);
switch (afterGPSfixMenu) {
case 'destNearest':
loadNearest(fix.lat, fix.lon);
break;
case 'createUserWaypoint':
if (!('userWaypoints' in settings))
settings.userWaypoints = [];
let newIdx = settings.userWaypoints.length;
settings.userWaypoints[newIdx] = {
'ID': 'USER'+(newIdx + 1),
'lat': fix.lat,
'lon': fix.lon,
};
writeSettings();
showUserWaypoints();
break;
case 'destAVWX':
// the free ("hobby") account of AVWX is limited to 10 nearest stations
avwx.request('station/near/'+fix.lat+','+fix.lon, 'n=10&airport=true&reporting=false', data => {
loadAVWX(data);
}, error => {
console.log(error);
E.showPrompt('AVWX query failed: '+error, {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => {
createDestMainMenu();
});
});
break;
default:
back();
}
}
// find nearest airports
function loadNearest(lat, lon) {
if (! readAirportsList(createDestMainMenu))
return;
const latRad1 = lat * Math.PI/180;
const lonRad1 = lon * Math.PI/180;
for (let i = 0; i < airports.length; i++) {
const latRad2 = airports[i].la * Math.PI/180;
const lonRad2 = airports[i].lo * Math.PI/180;
let x = (lonRad2 - lonRad1) * Math.cos((latRad1 + latRad2) / 2);
let y = (latRad2 - latRad1);
airports[i].distance = Math.sqrt(x*x + y*y) * 6371;
}
let nearest = airports.sort((a, b) => a.distance - b.distance).slice(0, 14);
let destNearest = {
'' : { 'title' : 'Nearest' },
'< Back' : () => createDestMainMenu(),
};
let airportCBs = [];
for (let i in nearest) {
let airport = nearest[i];
eval('airportCBs['+i+'] = function() { '+
'settings.destID = "'+airport.i+'"; '+
'settings.destLat = "'+airport.la+'"; '+
'settings.destLon = "'+airport.lo+'"; '+
'writeSettings(); '+
'createDestMainMenu(); '+
'}');
destNearest[airport.i+' - '+airport.n] = airportCBs[i];
}
E.showMenu(destNearest);
}
// process the data returned by AVWX
function loadAVWX(data) {
let AVWXairports = JSON.parse(data.resp);
let destAVWX = {
'' : { 'title' : 'Nearest (AVWX)' },
'< Back' : () => createDestMainMenu(),
};
let airportCBs = [];
for (let i in AVWXairports) {
let airport = AVWXairports[i].station;
let airport_id = ( airport.icao ? airport.icao : airport.gps );
eval('airportCBs['+i+'] = function() { '+
'settings.destID = "'+airport_id+'"; '+
'settings.destLat = "'+airport.latitude+'"; '+
'settings.destLon = "'+airport.longitude+'"; '+
'writeSettings(); '+
'createDestMainMenu(); '+
'}');
destAVWX[airport_id+' - '+airport.name] = airportCBs[i];
}
E.showMenu(destAVWX);
}
// individual user waypoint menu
function showUserWaypoint(idx) {
let wayptID = settings.userWaypoints[idx].ID;
let wayptLat = settings.userWaypoints[idx].lat;
let wayptLon = settings.userWaypoints[idx].lon;
let destUser = {
'' : { 'title' : wayptID },
'< Back' : () => showUserWaypoints(),
};
eval('let wayptUseCB = function() { '+
'settings.destID = "'+wayptID+'"; '+
'settings.destLat = "'+wayptLat+'"; '+
'settings.destLon = "'+wayptLon+'"; '+
'writeSettings(); '+
'createDestMainMenu(); '+
'}');
destUser['Set as Dest.'] = wayptUseCB;
destUser['Edit ID'] = function() {
require('textinput').input({text: wayptID}).then(result => {
if (result) {
if (result.length > 7) {
console.log('test');
E.showPrompt('ID is too long!\n(max. 7 chars)',
{title: 'Flight-Dash', buttons: {OK: true} }).then((v) => {
showUserWaypoint(idx);
});
} else {
settings.userWaypoints[idx].ID = result;
writeSettings();
showUserWaypoint(idx);
}
} else {
showUserWaypoint(idx);
}
});
};
destUser['Delete'] = function() {
E.showPrompt('Delete user waypoint '+wayptID+'?',
{'title': 'Flight-Dash'}).then((v) => {
if (v) {
settings.userWaypoints.splice(idx, 1);
writeSettings();
showUserWaypoints();
} else {
showUserWaypoint(idx);
}
});
};
E.showMenu(destUser);
}
// user waypoints menu
function showUserWaypoints() {
let destUser = {
'' : { 'title' : 'User Waypoints' },
'< Back' : () => createDestMainMenu(),
};
let wayptCBs = [];
for (let i in settings.userWaypoints) {
let waypt = settings.userWaypoints[i];
eval('wayptCBs['+i+'] = function() { showUserWaypoint('+i+'); }');
destUser[waypt.ID] = wayptCBs[i];
}
destUser['Create New'] = function() {
E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'});
afterGPSfixMenu = 'createUserWaypoint';
Bangle.setGPSPower(true, APP_NAME+'-settings');
Bangle.on('GPS', getLatLon);
};
E.showMenu(destUser);
}
// destination main menu
function createDestMainMenu() {
let destMainMenu = {
'' : { 'title' : 'Nav Dest.' },
'< Back' : () => E.showMenu(mainMenu),
};
destMainMenu['Is: '+settings.destID] = {};
destMainMenu['Nearest'] = function() {
E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'});
afterGPSfixMenu = 'destNearest';
Bangle.setGPSPower(true, APP_NAME+'-settings');
Bangle.on('GPS', getLatLon);
};
destMainMenu['Search'] = function() {
require('textinput').input({text: ''}).then(result => {
if (result) {
if (! readAirportsList(createDestMainMenu))
return;
result = result.toUpperCase();
let matches = [];
let tooManyFound = false;
for (let i in airports) {
if (airports[i].i.toUpperCase().includes(result) ||
airports[i].n.toUpperCase().includes(result)) {
matches.push(airports[i]);
if (matches.length >= 15) {
tooManyFound = true;
break;
}
}
}
if (! matches.length) {
E.showPrompt('No airports found!', {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => {
createDestMainMenu();
});
return;
}
let destSearch = {
'' : { 'title' : 'Search Results' },
'< Back' : () => createDestMainMenu(),
};
let airportCBs = [];
for (let i in matches) {
let airport = matches[i];
eval('airportCBs['+i+'] = function() { '+
'settings.destID = "'+airport.i+'"; '+
'settings.destLat = "'+airport.la+'"; '+
'settings.destLon = "'+airport.lo+'"; '+
'writeSettings(); '+
'createDestMainMenu(); '+
'}');
destSearch[airport.i+' - '+airport.n] = airportCBs[i];
}
if (tooManyFound) {
destSearch['More than 15 airports found!'] = {};
}
E.showMenu(destSearch);
} else {
createDestMainMenu();
}
});
};
destMainMenu['User waypts'] = function() { showUserWaypoints(); };
if (avwx) {
destMainMenu['Nearest (AVWX)'] = function() {
E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'});
afterGPSfixMenu = 'destAVWX';
Bangle.setGPSPower(true, APP_NAME+'-settings');
Bangle.on('GPS', getLatLon);
};
}
E.showMenu(destMainMenu);
}
// main menu
mainMenu = {
'' : { 'title' : 'Flight-Dash' },
'< Back' : () => {
Bangle.setGPSPower(false, APP_NAME+'-settings');
Bangle.removeListener('GPS', getLatLon);
back();
},
'Nav Dest.': () => createDestMainMenu(),
'Speed': {
value: parseInt(settings.speedUnits) || 0,
min: 0,
max: 2,
format: v => {
switch (v) {
case 0: return 'Knots';
case 1: return 'km/h';
case 2: return 'MPH';
}
},
onchange: v => {
settings.speedUnits = v;
writeSettings();
}
},
'Altitude': {
value: parseInt(settings.altimeterUnits) || 0,
min: 0,
max: 1,
format: v => {
switch (v) {
case 0: return 'Feet';
case 1: return 'Meters';
}
},
onchange: v => {
settings.altimeterUnits = v;
writeSettings();
}
},
'Use Baro': {
value: !!settings.useBaro, // !! converts undefined to false
format: v => v ? 'On' : 'Off',
onchange: v => {
settings.useBaro = v;
writeSettings();
}
},
};
E.showMenu(mainMenu);
})

View File

@ -0,0 +1,186 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="jquery-csv.min.js"></script>
</head>
<body>
<p>You can upload a list of airports, which can then be used as the
navigation destinations in the Flight-Dash. It is recommended to only
upload up to 100 - 150 airports max. Due to memory contraints on the
Bangle, no more than 500 airports can be uploaded.</p>
<p>The database of airports is based on <a href="https://ourairports.com/data/">OurAirports</a>.
<h2>Filter Airports</h2>
<div class="form-group row">
<label for="filter_range">Within:</label>
<input type="text" id="filter_range" size="4" />nm of
<label for="filter_lat">Lat:</label>
<input type="text" id="filter_lat" size="10" /> /
<label for="filter_lon">Lon:</label>
<input type="text" id="filter_lon" size="10" />
<div>
<small class="text-muted">This is using a simple lat/lon "block" - and
not within a proper radius around the given lat/lon position. An easy
way to find a lat/lon pair is to search for an airport based on ident
or name, and then use the found coordinates.</small>
</div>
</div>
<p>- or -</p>
<p>
<label for="filter_ident">Ident:</label>
<input type="text" id="filter_ident" />
</p>
<p>- or -</p>
<p>
<label for="filter_name">Name:</label>
<input type="text" id="filter_name" />
</p>
<p>Only 1 of the above filters is applied, with higher up in the list taking precedence.</p>
<div class="form-group row">
<label for="filter_country">Limit airports to within this country:</label>
<input type="text" id="filter_country" size="2" />
<div>
<small class="form-text text-muted">Use the
<a href="https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes">ISO-3166 2-letter code</a>,
eg. "AU"</small>
</div>
</div>
<p>
<button id="getAndFilter" class="btn btn-primary" onClick="getAndFilter();">Filter</button>
<button id="uploadButton" class="btn btn-primary" onClick="uploadAirports();" style="display: none;">Upload to Bangle</button>
</p>
<hr />
<h2>Results:</h2>
<p><div id="status"></div></p>
<div id="resultsTable"></div>
<script src="../../core/lib/interface.js"></script>
<script>
var airports = [];
function getAndFilter() {
let filterRange = $("#filter_range").val();
let filterLat = $("#filter_lat").val();
let filterLatMin, filterLatMax;
let filterLon = $("#filter_lon").val();
let filterLonMin, filterLonMax;
let filterIdent = $("#filter_ident").val().toUpperCase();
let filterName = $("#filter_name").val().toUpperCase();
let filterCountry = $("#filter_country").val().toUpperCase();
if (filterRange && (! filterLat || ! filterLon)) {
alert('When filtering by Range, set both a Latitude and a Longitude!');
return;
}
if (filterRange) {
filterLatMin = parseFloat(filterLat) - (parseInt(filterRange) / 60);
filterLatMax = parseFloat(filterLat) + (parseInt(filterRange) / 60);
filterLonMin = parseFloat(filterLon) - (parseInt(filterRange) / 60);
filterLonMax = parseFloat(filterLon) + (parseInt(filterRange) / 60);
}
$("#status").html($("<em>").text('Fetching and filtering airports ...'));
$.get('https://davidmegginson.github.io/ourairports-data/airports.csv', function (data) {
let allAirports = $.csv.toObjects(data);
airports = allAirports.filter((item) => {
if (filterRange) {
let lat = parseFloat(item.latitude_deg);
let lon = parseFloat(item.longitude_deg);
if (lat > filterLatMin && lat < filterLatMax &&
lon > filterLonMin && lon < filterLonMax) {
if (filterCountry) {
return item.iso_country == filterCountry;
} else {
return true;
}
} else {
return false;
}
}
if (filterIdent) {
if (item.ident.toUpperCase().includes(filterIdent)) {
if (filterCountry) {
return item.iso_country == filterCountry;
} else {
return true;
}
} else {
return false;
}
}
if (filterName) {
if (item.name.toUpperCase().includes(filterName)) {
if (filterCountry) {
return item.iso_country == filterCountry;
} else {
return true;
}
} else {
return false;
}
}
if (filterCountry) {
return item.iso_country == filterCountry;
}
}).map((item) => {
return {
'i': item.ident,
'n': item.name,
'la': item.latitude_deg,
'lo': item.longitude_deg
};
});
let container = $("#resultsTable");
if (airports.length == 0) {
$("#status").html($("<strong>").text('No airports matched the filter criteria!'));
return;
} else if (airports.length > 500) {
$("#status").html($("<strong>").text(airports.length+' airports matched the filter criteria - your Bangle can only handle a maximum of 500!'));
return;
} else if (airports.length > 150) {
$("#status").html($("<strong>").text(airports.length+" airports matched the filter criteria - your Bangle will struggle with more than 150 airports. You can try, but it's recommended to reduce the number of airports."));
}
container.html($("<p>").text('Number of matching airports: '+airports.length));
let table = $("<table>");
table.addClass('table');
let cols = Object.keys(airports[0]);
$.each(airports, function(i, item){
let tr = $("<tr>");
let vals = Object.values(item);
$.each(vals, (i, elem) => {
tr.append($("<td>").text(elem));
});
table.append(tr);
});
container.append(table)
$("#status").html('');
$("#uploadButton").show();
});
}
function uploadAirports() {
$("#status").html($("<em>").text('Uploading airports to Bangle ...'));
Util.writeStorage('flightdash.airports.json', JSON.stringify(airports), () => {
$('#status').html('Airports successfully uploaded to Bangle!');
});
}
</script>
</body>
</html>

1
apps/flightdash/jquery-csv.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
{
"id": "flightdash",
"name": "Flight Dashboard",
"shortName":"Flight-Dash",
"version":"1.00",
"description": "Basic flight and navigation instruments",
"icon": "flightdash.png",
"screenshots": [{ "url": "screenshot.png" }],
"type": "app",
"tags": "outdoors",
"supports": ["BANGLEJS2"],
"dependencies": { "textinput": "type" },
"readme": "README.md",
"interface": "interface.html",
"storage": [
{ "name":"flightdash.app.js", "url":"flightdash.app.js" },
{ "name":"flightdash.settings.js", "url":"flightdash.settings.js" },
{ "name":"flightdash.img", "url":"flightdash-icon.js", "evaluate":true }
],
"data": [{ "name":"flightdash.json" },{ "name":"flightdash.airports.json" }]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB