mirror of https://github.com/espruino/BangleApps
Merge branch 'espruino:master' into master
commit
69257dd469
|
@ -3,7 +3,7 @@
|
|||
"shortName":"3DClock",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "This is a simple 3D scalig demo based on Anton Clock",
|
||||
"description": "This is a simple 3D scaling demo based on Anton Clock",
|
||||
"screenshots" : [ { "url":"screenshot.png" }],
|
||||
"type":"clock",
|
||||
"tags": "clock",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
1.00: initial release
|
|
@ -0,0 +1,76 @@
|
|||
# Flight Dashboard
|
||||
|
||||
Shows basic flight and navigation instruments.
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4A/AHcCkAX/C/4X9kUiC/4XcgczmcwRSArBkYWBAAMyA4KUMC4QWDAAIXOAAUziMTmMRmZdRmQXDkZhIHQJ1IAAYXGBgoNDgQJFLoQhFDQ84wQFDlGDBwxBInGIDAUoxAXFJosDOIIXDAAgXCPoJkGBAKfBmc6C4ujBIINBiYXIEIMK1AWDxWgHoQXMgGqC4eqKoYXHL4QFChQYC1QuBEwbcHZo7hHBpYA/AH4A/AH4"))
|
|
@ -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 |
|
@ -0,0 +1,328 @@
|
|||
(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);
|
||||
}
|
||||
|
||||
// update the nav destination
|
||||
function updateNavDest(destID, destLat, destLon) {
|
||||
settings.destID = destID.replace(/[\W]+/g, '').slice(0, 7);
|
||||
settings.destLat = parseFloat(destLat);
|
||||
settings.destLon = parseFloat(destLon);
|
||||
writeSettings();
|
||||
createDestMainMenu();
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
for (let i in nearest) {
|
||||
let airport = nearest[i];
|
||||
destNearest[airport.i+' - '+airport.n] =
|
||||
() => setTimeout(updateNavDest, 10, airport.i, airport.la, airport.lo);
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
for (let i in AVWXairports) {
|
||||
let airport = AVWXairports[i].station;
|
||||
let airport_id = ( airport.icao ? airport.icao : airport.gps );
|
||||
destAVWX[airport_id+' - '+airport.name] =
|
||||
() => setTimeout(updateNavDest, 10, airport_id, airport.latitude, airport.longitude);
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
destUser['Set as Dest.'] =
|
||||
() => setTimeout(updateNavDest, 10, wayptID, wayptLat, wayptLon);
|
||||
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(),
|
||||
};
|
||||
for (let i in settings.userWaypoints) {
|
||||
let waypt = settings.userWaypoints[i];
|
||||
let idx = i;
|
||||
destUser[waypt.ID] =
|
||||
() => setTimeout(showUserWaypoint, 10, idx);
|
||||
}
|
||||
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(),
|
||||
};
|
||||
for (let i in matches) {
|
||||
let airport = matches[i];
|
||||
destSearch[airport.i+' - '+airport.n] =
|
||||
() => setTimeout(updateNavDest, 10, airport.i, airport.la, airport.lo);
|
||||
}
|
||||
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);
|
||||
})
|
|
@ -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>
|
File diff suppressed because one or more lines are too long
|
@ -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 |
|
@ -13,6 +13,7 @@ const speedUnits = { // how many kph per X?
|
|||
"kph": 1,
|
||||
"km/h": 1,
|
||||
"kmt": 1,
|
||||
"km/t": 1,
|
||||
"km/tim": 1,
|
||||
"mph": 1.60934,
|
||||
"kts": 1.852
|
||||
|
@ -318,6 +319,42 @@ var locales = {
|
|||
day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday",
|
||||
// No translation for english...
|
||||
},
|
||||
"da_DK": {
|
||||
lang: "da_DK",
|
||||
decimal_point: ",",
|
||||
thousands_sep: ".",
|
||||
currency_symbol: "kr",
|
||||
int_curr_symbol: "DKK",
|
||||
speed: "km/t",
|
||||
distance: { 0: "m", 1: "km" },
|
||||
temperature: "°C",
|
||||
ampm: { 0: "", 1: "" },
|
||||
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
|
||||
datePattern: { 0: "%d. %b. %Y", 1: "%d/%m %Y" }, // 1. feb. 2020 // 01/02 2020 // a better short ver. is 1/2 2020 but its not supported
|
||||
abmonth: "jan,feb,mar,apr,maj,jun,jul,aug,sep,okt,nov,dec",
|
||||
month: "januar,februar,marts,april,maj,juni,juli,august,september,oktober,november,december",
|
||||
abday: "søn,man,tir,ons,tor,fre,lør",
|
||||
day: "søndag,mandag,tirsdag,onsdag,torsdag,fredag,lørdag",
|
||||
trans: { yes: "ja", Yes: "Ja", no: "nej", No: "Nej", ok: "ok", on: "tændt", off: "slukket" } // no single danish translation for "on"/"off", should not be used
|
||||
},
|
||||
"en_DK": { // Danish units with english language
|
||||
lang: "en_DK",
|
||||
decimal_point: ",",
|
||||
thousands_sep: ".",
|
||||
currency_symbol: "kr",
|
||||
int_curr_symbol: "DKK",
|
||||
speed: "km/h",
|
||||
distance: { 0: "m", 1: "km" },
|
||||
temperature: "°C",
|
||||
ampm: { 0: "", 1: "" },
|
||||
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
|
||||
datePattern: { 0: "%d. %b. %Y", 1: "%d/%m %Y" }, // 1. feb. 2020 // 01/02 2020 // a better short ver. is 1/2 2020 but its not supported
|
||||
abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
|
||||
month: "January,February,March,April,May,June,July,August,September,October,November,December",
|
||||
abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat",
|
||||
day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday",
|
||||
// No translation for english...
|
||||
},
|
||||
"en_NZ": {
|
||||
lang: "en_NZ",
|
||||
decimal_point: ".",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: Initial Medical Information application!
|
||||
0.02: Read height and weight from myprofile
|
||||
|
|
|
@ -10,12 +10,12 @@ The file has the following contents:
|
|||
```
|
||||
{
|
||||
"bloodType": "",
|
||||
"height": "",
|
||||
"weight": "",
|
||||
"medicalAlert": [ "" ]
|
||||
}
|
||||
```
|
||||
|
||||
Weight and height are read from myprofile.
|
||||
|
||||
## Medical information editor
|
||||
|
||||
Clicking on the download icon of `Medical Information` in the app loader invokes the editor.
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
const medicalinfo = require('medicalinfo').load();
|
||||
const myprofile = require("Storage").readJSON("myprofile.json",1)||{};
|
||||
// const medicalinfo = {
|
||||
// bloodType: "O+",
|
||||
// height: "166cm",
|
||||
// weight: "73kg"
|
||||
// };
|
||||
|
||||
function hasAlert(info) {
|
||||
|
@ -12,7 +11,7 @@ function hasAlert(info) {
|
|||
// No space for widgets!
|
||||
// TODO: no padlock widget visible so prevent screen locking?
|
||||
|
||||
g.clear();
|
||||
g.reset().clear();
|
||||
const bodyFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
|
||||
g.setFont(bodyFont);
|
||||
|
||||
|
@ -33,10 +32,14 @@ if (hasAlert(medicalinfo)) {
|
|||
if (medicalinfo.bloodType) {
|
||||
lines = lines.concat(g.wrapString("Blood group: " + medicalinfo.bloodType, g.getWidth() - 10));
|
||||
}
|
||||
if (medicalinfo.height) {
|
||||
if (myprofile.height) { // Prefer height from myprofile if set
|
||||
lines = lines.concat(g.wrapString("Height: " + require("locale").distance(myprofile.height, 2), g.getWidth() - 10));
|
||||
} else if (medicalinfo.height) { // read height from own settings if previously stored here
|
||||
lines = lines.concat(g.wrapString("Height: " + medicalinfo.height, g.getWidth() - 10));
|
||||
}
|
||||
if (medicalinfo.weight) {
|
||||
if (myprofile.weight) { // Prefer weight from myprofile if set
|
||||
lines = lines.concat(g.wrapString("Weight: " + myprofile.weight + "kg", g.getWidth() - 10));
|
||||
} else if (medicalinfo.weight) { // read weight from own settings if previously stored here
|
||||
lines = lines.concat(g.wrapString("Weight: " + medicalinfo.weight, g.getWidth() - 10));
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ const storage = require('Storage');
|
|||
exports.load = function () {
|
||||
const medicalinfo = storage.readJSON('medicalinfo.json') || {
|
||||
bloodType: "",
|
||||
height: "",
|
||||
weight: "",
|
||||
medicalAlert: [""]
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"bloodType": "",
|
||||
"height": "",
|
||||
"weight": "",
|
||||
"medicalAlert": [ "" ]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "medicalinfo",
|
||||
"name": "Medical Information",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Provides 'medicalinfo.json' used by various health apps, as well as a way to edit it from the App Loader",
|
||||
"icon": "app.png",
|
||||
"tags": "health,medical",
|
||||
|
@ -16,5 +16,6 @@
|
|||
],
|
||||
"data": [
|
||||
{"name":"medicalinfo.json","url":"medicalinfo.json"}
|
||||
]
|
||||
],
|
||||
"dependencies": {"myprofile":"app"}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Add weight
|
|
@ -10,6 +10,7 @@ Configure your personal profile. All settings are optional and are only stored o
|
|||
| HR max | maximum heart rate | BPM | BPM | 60 | Use maximum value when exercising.<br/> If unsure set to 220-age. |
|
||||
| HR min | minimum heart rate | BPM | BPM | 200 | Measure your heart rate after waking up |
|
||||
| Height | Body height | local length unit | meter | 0 (=not set) | - |
|
||||
| Weight | Body weight | kg | kf | 0 (=not set) | - |
|
||||
| Stride length | distance traveled with one step | local length unit | meter | 0 (=not set) | Walk 10 steps and divide the travelled distance by 10 |
|
||||
|
||||
## Developer notes
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css" />
|
||||
|
||||
<style type="text/css">
|
||||
.alert {
|
||||
padding: 20px;
|
||||
background-color: #f44336; /* Red */
|
||||
color: white;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="info"></div>
|
||||
|
||||
<button id="btnReload" class="btn btn-primary">Reload from watch</button>
|
||||
<button id="btnUpload" class="btn btn-primary">Upload to watch</button>
|
||||
<button id="btnDownload" class="btn btn-primary">Download</button>
|
||||
|
||||
<pre id="myprofile" contenteditable></pre>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script>
|
||||
const myProfileFile = "myprofile.json";
|
||||
|
||||
function errorFormat() {
|
||||
var date = new Date();
|
||||
var error =
|
||||
'<p class="alert">' +
|
||||
date.toUTCString() +
|
||||
" : Wrong format, it should be JSON" +
|
||||
"</p>";
|
||||
return error;
|
||||
}
|
||||
|
||||
function getEditableContent() {
|
||||
return document.getElementById("myprofile").innerHTML.replace(/<[^>]*>/g, '');;
|
||||
}
|
||||
|
||||
function isJsonString(str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
console.log(str)
|
||||
console.log(e)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function uploadFile(fileid, contents) {
|
||||
Puck.write(
|
||||
`\x10(function() {
|
||||
require("Storage").write("${fileid}",'${contents}');
|
||||
Bluetooth.print("OK");
|
||||
})()\n`,
|
||||
(ret) => {
|
||||
console.log("uploadFile", ret);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* Load settings JSON file from the watch.
|
||||
*/
|
||||
function loadMyProfile() {
|
||||
document.getElementById("info").innerHTML = "";
|
||||
Util.showModal("Loading...");
|
||||
Puck.eval(`require('Storage').readJSON("${myProfileFile}")`, (data) => {
|
||||
document.getElementById("myprofile").innerHTML = JSON.stringify(
|
||||
data,
|
||||
null,
|
||||
2
|
||||
);
|
||||
Util.hideModal();
|
||||
});
|
||||
}
|
||||
/* Save settings as a JSON file on the watch.
|
||||
*/
|
||||
function uploadMyProfile() {
|
||||
document.getElementById("info").innerHTML = "";
|
||||
Util.showModal("Uploading...");
|
||||
let myProfileJson = getEditableContent();
|
||||
if (isJsonString(myProfileJson)) {
|
||||
let shortMedicalInfoJson = JSON.stringify(JSON.parse(myProfileJson));
|
||||
uploadFile(myProfileFile, shortMedicalInfoJson);
|
||||
} else {
|
||||
document.getElementById("info").innerHTML = errorFormat();
|
||||
}
|
||||
Util.hideModal();
|
||||
}
|
||||
|
||||
function downloadMyProfile() {
|
||||
document.getElementById("info").innerHTML = "";
|
||||
Util.showModal("Downloading...");
|
||||
let myProfileJson = getEditableContent();
|
||||
if (isJsonString(myProfileJson)) {
|
||||
Util.saveFile(myProfileFile, "application/json", myProfileJson);
|
||||
} else {
|
||||
document.getElementById("info").innerHTML = errorFormat();
|
||||
}
|
||||
Util.hideModal();
|
||||
}
|
||||
|
||||
document
|
||||
.getElementById("btnUpload")
|
||||
.addEventListener("click", function () {
|
||||
uploadMyProfile();
|
||||
});
|
||||
document
|
||||
.getElementById("btnDownload")
|
||||
.addEventListener("click", function () {
|
||||
downloadMyProfile();
|
||||
});
|
||||
document
|
||||
.getElementById("btnReload")
|
||||
.addEventListener("click", function () {
|
||||
loadMyProfile();
|
||||
});
|
||||
function onInit() {
|
||||
loadMyProfile();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -3,11 +3,12 @@
|
|||
"shortName":"My Profile",
|
||||
"icon": "app.png",
|
||||
"type": "settings",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Configure your personal profile. All settings are optional and only stored on the watch.",
|
||||
"readme": "README.md",
|
||||
"tags": "tool,utility",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"interface": "interface.html",
|
||||
"storage": [
|
||||
{"name":"myprofile.settings.js","url":"settings.js"}
|
||||
],
|
||||
|
@ -15,4 +16,3 @@
|
|||
{"name":"myprofile.json"}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
strideLength: 0, // 0 = not set
|
||||
birthday: '1970-01-01',
|
||||
height: 0, // 0 = not set
|
||||
weight: 0, // 0 = not set
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeProfile() {
|
||||
|
@ -101,6 +102,17 @@
|
|||
}
|
||||
},
|
||||
|
||||
/*LANG*/"Weight": {
|
||||
value: myprofile.weight,
|
||||
min:0,
|
||||
step:1,
|
||||
format: v => v ? v + "kg" : '-',
|
||||
onchange: v => {
|
||||
myprofile.weight=v;
|
||||
writeProfile();
|
||||
},
|
||||
},
|
||||
|
||||
/*LANG*/'HR max': {
|
||||
format: v => /*LANG*/`${v} BPM`,
|
||||
value: myprofile.maxHrm,
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New widget
|
||||
0.02: Use default Bangle formatter for booleans
|
||||
0.03: Add option to hide widget
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "widadjust",
|
||||
"name": "Adjust Clock",
|
||||
"icon": "icon.png",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "Adjusts clock continually in the background to counter clock drift",
|
||||
"type": "widget",
|
||||
"tags": "widget",
|
||||
|
|
|
@ -108,6 +108,11 @@
|
|||
value: settings.debugLog,
|
||||
onchange: v => settings.debugLog = v,
|
||||
},
|
||||
|
||||
'Hide Widget': {
|
||||
value: settings.hide || false,
|
||||
onchange: v => settings.hide = v,
|
||||
},
|
||||
};
|
||||
|
||||
E.showMenu(mainMenu);
|
||||
|
|
|
@ -78,13 +78,16 @@
|
|||
}
|
||||
|
||||
function debug(line) {
|
||||
console.log(line);
|
||||
//console.log(line);
|
||||
if (debugLogFile !== null) {
|
||||
debugLogFile.write(line + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
if (settings.hide === true) {
|
||||
return;
|
||||
}
|
||||
g.reset().setFont('6x8').setFontAlign(0, 0);
|
||||
g.clearRect(this.x, this.y, this.x + WIDTH - 1, this.y + 23);
|
||||
g.drawString(Math.round(clockError), this.x + WIDTH/2, this.y + 9);
|
||||
|
@ -208,7 +211,7 @@
|
|||
let updatedClockError = clockError + (now - lastClockErrorUpdateTime) * ppm / 1000000;
|
||||
return now - updatedClockError;
|
||||
},
|
||||
width: WIDTH,
|
||||
width: settings.hide === true ? 0 : WIDTH,
|
||||
};
|
||||
|
||||
if (settings.saveState) {
|
||||
|
|
|
@ -9,3 +9,5 @@
|
|||
0.06: Fix exception
|
||||
0.07: Ensure barometer gets turned off after a few readings (isBarometerOn broken in 2v16)
|
||||
0.08: Compatibility with hideable Widgets
|
||||
0.09: Do not immediately measure on start, keep interval
|
||||
Add plot history to settings
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "widbaroalarm",
|
||||
"name": "Barometer Alarm Widget",
|
||||
"shortName": "Barometer Alarm",
|
||||
"version": "0.08",
|
||||
"version": "0.09",
|
||||
"description": "A widget that can alarm on when the pressure reaches defined thresholds.",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
|
|
|
@ -107,9 +107,52 @@
|
|||
return x + " min";
|
||||
}
|
||||
},
|
||||
'Plot history': () => {E.showMenu(); draw();},
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const history3 = require('Storage').readJSON("widbaroalarm.log.json", true) || []; // history of recent 3 hours
|
||||
|
||||
const now = new Date()/(1000);
|
||||
let curtime = now-3*60*60; // 3h ago
|
||||
const data = [];
|
||||
while (curtime <= now) {
|
||||
// find closest value in history for this timestamp
|
||||
const closest = history3.reduce((prev, curr) => {
|
||||
return (Math.abs(curr.ts - curtime) < Math.abs(prev.ts - curtime) ? curr : prev);
|
||||
});
|
||||
data.push(closest.p);
|
||||
curtime += settings.interval*60;
|
||||
}
|
||||
|
||||
Bangle.setUI({
|
||||
mode: "custom",
|
||||
back: () => showMainMenu(),
|
||||
});
|
||||
|
||||
g.reset().setFont("6x8",1);
|
||||
require("graph").drawLine(g, data, {
|
||||
axes: true,
|
||||
x: 4,
|
||||
y: Bangle.appRect.y+8,
|
||||
height: Bangle.appRect.h-20,
|
||||
gridx: 1,
|
||||
gridy: 1,
|
||||
miny: Math.min.apply(null, data)-1,
|
||||
maxy: Math.max.apply(null, data)+1,
|
||||
title: /*LANG*/"Barometer history (mBar)",
|
||||
ylabel: y => y,
|
||||
xlabel: i => {
|
||||
const t = -3*60 + settings.interval*i;
|
||||
if (t % 60 === 0) {
|
||||
return "-" + t/60 + "h";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
showMainMenu();
|
||||
});
|
||||
|
|
|
@ -293,8 +293,18 @@ WIDGETS["baroalarm"] = {
|
|||
draw : draw
|
||||
};
|
||||
|
||||
if (interval > 0) {
|
||||
setInterval(getPressureValue, interval * 60000);
|
||||
// delay pressure measurement by interval-lastrun
|
||||
const lastRun = history3.length > 0 ? history3[history3.length-1].ts : 0;
|
||||
const lastRunAgo = Math.round(Date.now() / 1000) - lastRun;
|
||||
let diffNextRun = interval*60-lastRunAgo;
|
||||
if (diffNextRun < 0) {
|
||||
diffNextRun = 0; // run asap
|
||||
}
|
||||
getPressureValue();
|
||||
setTimeout(() => {
|
||||
if (interval > 0) {
|
||||
setInterval(getPressureValue, interval * 60000);
|
||||
}
|
||||
getPressureValue();
|
||||
}, diffNextRun*1000);
|
||||
|
||||
})();
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
{
|
||||
"//": "Catalan language translations",
|
||||
"GLOBAL": {
|
||||
"//": "Translations that apply for all apps",
|
||||
"Alarms": "Alarmes",
|
||||
"Hours": "Hores",
|
||||
"Minutes": "Minuts",
|
||||
"Enabled": "Activat",
|
||||
"Save": "Desa",
|
||||
"Back": "Enrere",
|
||||
"Repeat": "Repetició",
|
||||
"Delete": "Esborra",
|
||||
"ALARM!": "ALARMA!",
|
||||
"Sleep": "Dormir",
|
||||
"New Alarm": "Alarma nova",
|
||||
"Yes": "Sí",
|
||||
"No": "No",
|
||||
"On": "Encès",
|
||||
"Off": "Apagat",
|
||||
"Ok": "D'acord",
|
||||
"(repeat)": "(repeteix)",
|
||||
"New Timer": "Temporitzador nou",
|
||||
"music": "música",
|
||||
"circle 2": "cercle 2",
|
||||
"circle 1": "cercle 1",
|
||||
"Keep Msgs": "Mantén Msgs",
|
||||
"circle 3": "cercle 3",
|
||||
"week": "setmana",
|
||||
"Auto snooze": "Repetició automàtica de l'alarma",
|
||||
"show widgets": "Mostra ginys",
|
||||
"min. confidence": "confiança mínima",
|
||||
"circle 4": "cercle 4",
|
||||
"circle count": "recompte de cercles",
|
||||
"heartrate": "ritme cardíac",
|
||||
"Heartrate": "Ritme cardíac",
|
||||
"weather circle": "cercle meteorològic",
|
||||
"battery warn": "avís de bateria",
|
||||
"minimum": "mínim",
|
||||
"distance goal": "objectiu de distància",
|
||||
"valid period": "període de validesa",
|
||||
"maximum": "màxim",
|
||||
"step length": "longitud del pas",
|
||||
"data": "dades",
|
||||
"colorize icon": "acoloriment de la icona",
|
||||
"Circle": "Cercle",
|
||||
"Launcher Settings": "Configuració del llançador",
|
||||
"App Source\nNot found": "Fuent de l'aplicació\nNo s'ha trobat",
|
||||
"Show clocks": "Mostra rellotges",
|
||||
"Font": "Font",
|
||||
"TAP right top/bottom": "TAP dret superior/inferior",
|
||||
"Yes\ndefinitely": "Sí\ndefinitivament",
|
||||
"View Message": "Veure missatges",
|
||||
"Delete all messages": "Esborra tots els missatges",
|
||||
"STEPS": "PASSOS",
|
||||
"BTNs 1:startlap 2:exit 3:reset": "BTNs 1:startlap 2:exit 3:reset",
|
||||
"start&lap/reset, BTN1: EXIT": "start&lap/reset, BTN1: EXIT",
|
||||
"Are you sure": "N'esteu segur",
|
||||
"Vector font size": "Mida de la font vectorial",
|
||||
"Mark Unread": "Marca com a no llegit",
|
||||
"No Messages": "No hi ha missatges",
|
||||
"Delete All Messages": "Esborra tots els missatges",
|
||||
"LCD": "LCD",
|
||||
"Apps": "Aplicacions",
|
||||
"Unread timer": "Temporitzador no llegit",
|
||||
"Record Run": "Record de carrera",
|
||||
"Bluetooth": "Bluetooth",
|
||||
"Quiet Mode": "Mode silenciós",
|
||||
"Piezo": "Piezo",
|
||||
"Make Connectable": "Fes-ho connectable",
|
||||
"Programmable": "Programable",
|
||||
"Vibration": "Vibració",
|
||||
"Passkey BETA": "Passkey BETA",
|
||||
"Customize": "Personalitza",
|
||||
"HID": "HID",
|
||||
"Utils": "Utilitats",
|
||||
"Light BW": "Llum BW",
|
||||
"BLE": "BLE",
|
||||
"Dark BW": "BW fosc",
|
||||
"Background 2": "Fons 2",
|
||||
"Foreground 2": "Prime pla 2",
|
||||
"Foreground": "Primer pla",
|
||||
"Highlight BG": "Ressalta BG",
|
||||
"Connect device\nto add to\nwhitelist": "Connecta dispositiu\nper afegir-ho a la\nllista blanca",
|
||||
"Highlight FG": "Ressalta FG",
|
||||
"Background": "Fons",
|
||||
"Add Device": "Afegir dispositiu",
|
||||
"Remove": "Suprimeix",
|
||||
"Wake on BTN3": "Desperta en BTN3",
|
||||
"Twist Max Y": "Gir màx Y",
|
||||
"LCD Timeout": "Temps d'espera del LCD",
|
||||
"Twist Threshold": "Llindar de gir",
|
||||
"Wake on BTN2": "Desperta en BTN2",
|
||||
"Wake on BTN1": "Desperta en BTN1",
|
||||
"Wake on Twist": "Desperta en girar",
|
||||
"LCD Brightness": "Brillantor del LCD",
|
||||
"Log": "Registre",
|
||||
"Time Zone": "Fus horari",
|
||||
"Wake on FaceUp": "Desperta en cara amunt",
|
||||
"Wake on Touch": "Desperta en tocar",
|
||||
"Twist Timeout": "Temps d'espera en girar",
|
||||
"Compact Storage": "Emmagatzematge compacte",
|
||||
"Clock Style": "Estil del rellotge",
|
||||
"Storage": "Emmagatzematge",
|
||||
"Utilities": "Utilitats",
|
||||
"Compacting...\nTakes approx\n1 minute": "S'està compactant...\nTriga aproximadament\n1 minut",
|
||||
"Debug Info": "Informació de la depuració",
|
||||
"Rewrite Settings": "Reescriure la configuració",
|
||||
"Flatten Battery": "Descarrega la bateria",
|
||||
"Turn Off": "Apaga",
|
||||
"This will remove everything": "Això ho eliminarà tot",
|
||||
"Reset Settings": "Reinicia la configuració",
|
||||
"Month": "Mes",
|
||||
"Second": "Segon",
|
||||
"Date": "Data",
|
||||
"Reset to Defaults": "Reinicia als valors per defecte",
|
||||
"Hour": "Hora",
|
||||
"Flattening battery - this can take hours.\nLong-press button to cancel": "Descarrega la bateria - pot trigar hores.\nPremeu prolongadament el botó per cancel·lar",
|
||||
"Stay Connectable": "Mantén connectat",
|
||||
"Minute": "Minut",
|
||||
"No Clocks Found": "No s'ha trobat cap rellotge",
|
||||
"Connectable": "Connectable",
|
||||
"No app has settings": "Cap aplicació té configuració",
|
||||
"Invalid settings": "Ajustes no vàlids",
|
||||
"App Settings": "Configuració de l'aplicació",
|
||||
"Side": "Costat",
|
||||
"OFF": "APAGAT",
|
||||
"Sleep Phase Alarm": "Alarma de fase de son",
|
||||
"Widgets": "Ginys",
|
||||
"Left": "Esquerra",
|
||||
"Sort Order": "Criteri d'ordenació",
|
||||
"TIMER": "TEMPORITZADOR",
|
||||
"goal": "objectiu",
|
||||
"Right": "A la dreta",
|
||||
"on": "en",
|
||||
"Alarm": "Alarma",
|
||||
"Reset All": "Reinicia-ho tot",
|
||||
"Reset all widgets": "Reinicia tots els ginys",
|
||||
"Reset": "Reinicia",
|
||||
"Beep": "So",
|
||||
"System": "Sistema",
|
||||
"Locale": "Configuració regional",
|
||||
"Message": "Missatge",
|
||||
"Set Time": "Estableix l'hora",
|
||||
"Vibrate": "Vibració",
|
||||
"Alerts": "Alertes",
|
||||
"Timer": "Temporitzador",
|
||||
"Error in settings": "Error en la configuració",
|
||||
"Select Clock": "Selecciona rellotge",
|
||||
"Whitelist": "Llista blanca",
|
||||
"Disable": "Desactiva",
|
||||
"BACK": "ENRERE",
|
||||
"Factory Reset": "Restableix la configuració de fàbrica",
|
||||
"Connected": "Connectat",
|
||||
"ALARM": "ALARMA",
|
||||
"Messages": "Missatges",
|
||||
"Settings": "Configuració",
|
||||
"Show": "Mostra",
|
||||
"Hide": "Amaga",
|
||||
"steps": "passos",
|
||||
"back": "enrere",
|
||||
"Steps": "Passos",
|
||||
"Year": "Any",
|
||||
"Loading": "S'està carregant",
|
||||
"Music": "Música",
|
||||
"color": "color",
|
||||
"off": "apagat",
|
||||
"Theme": "Tema",
|
||||
"one": "un",
|
||||
"two": "dos",
|
||||
"three": "tres",
|
||||
"four": "quatre",
|
||||
"five": "cinc",
|
||||
"six": "sis",
|
||||
"seven": "set",
|
||||
"eight": "vuit",
|
||||
"nine": "nou",
|
||||
"ten": "deu",
|
||||
"eleven": "onze",
|
||||
"twelve": "dotze"
|
||||
},
|
||||
"alarm": {
|
||||
"//": "App-specific overrides",
|
||||
"rpt": "rep."
|
||||
},
|
||||
"fuzzyw": {
|
||||
"//": "App-specific overrides",
|
||||
"*$1 o'clock": "*$1 en punt",
|
||||
"five past *$1": "*$1 i cinc",
|
||||
"ten past *$1": "*$1 i deu",
|
||||
"quarter past *$1": "Un quart de *$2",
|
||||
"twenty past *$1": "Un quart i cinc de *$2",
|
||||
"twenty five past *$1": "Un quart i deu de *$2",
|
||||
"half past *$1": "Dos quarts de *$2",
|
||||
"twenty five to *$2": "Dos quarts i cinc de *$2",
|
||||
"twenty to *$2": "Dos quarts i deu de *$2",
|
||||
"quarter to *$2": "Tres quarts de *$2",
|
||||
"ten to *$2": "Tres quarts i cinc de *$2",
|
||||
"five to *$2": "Tres quarts ben tocats de *$2"
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
"ALARM!": "ALARM!",
|
||||
"Sleep": "Dormir",
|
||||
"New Alarm": "Nueva alarma",
|
||||
"Yes": "Si",
|
||||
"Yes": "Sí",
|
||||
"No": "No",
|
||||
"On": "Encendido",
|
||||
"Off": "Apagado",
|
||||
|
@ -76,13 +76,13 @@
|
|||
"Light BW": "Luz BW",
|
||||
"BLE": "BLE",
|
||||
"Dark BW": "BW oscuro",
|
||||
"Background 2": "Antecedentes 2",
|
||||
"Background 2": "Fondo 2",
|
||||
"Foreground 2": "Primer plano 2",
|
||||
"Foreground": "Primer plano",
|
||||
"Highlight BG": "Resaltar BG",
|
||||
"Connect device\nto add to\nwhitelist": "Conectar dispositivo\npara añadirlo a la\nlista blanca",
|
||||
"Highlight FG": "Destacar FG",
|
||||
"Background": "Antecedentes",
|
||||
"Background": "Fondo",
|
||||
"Add Device": "Añadir dispositivo",
|
||||
"Remove": "Eliminar",
|
||||
"Wake on BTN3": "Wake en BTN3",
|
||||
|
@ -101,11 +101,11 @@
|
|||
"Compact Storage": "Almacenamiento compacto",
|
||||
"Clock Style": "Estilo de reloj",
|
||||
"Storage": "Almacenamiento",
|
||||
"Utilities": "Servicios públicos",
|
||||
"Utilities": "Utilidades",
|
||||
"Compacting...\nTakes approx\n1 minute": "La compactación...\nTarda aproximadamente\n1 minuto",
|
||||
"Debug Info": "Información de depuración",
|
||||
"Rewrite Settings": "Reescribir la configuración",
|
||||
"Flatten Battery": "Aplastar la batería",
|
||||
"Flatten Battery": "Descargar la batería",
|
||||
"Turn Off": "Apagar",
|
||||
"This will remove everything": "Esto eliminará todo",
|
||||
"Reset Settings": "Restablecer la configuración",
|
||||
|
@ -114,9 +114,9 @@
|
|||
"Date": "Fecha",
|
||||
"Reset to Defaults": "Restablecer los valores predeterminados",
|
||||
"Hour": "Hora",
|
||||
"Flattening battery - this can take hours.\nLong-press button to cancel": "Aplastar la batería - esto puede llevar horas.\nPulsar prolongadamente el botón para cancelar",
|
||||
"Flattening battery - this can take hours.\nLong-press button to cancel": "Descargar la batería - esto puede llevar horas.\nPulsar prolongadamente el botón para cancelar",
|
||||
"Stay Connectable": "Manténgase conectado",
|
||||
"Minute": "Minuta",
|
||||
"Minute": "Minuto",
|
||||
"No Clocks Found": "No se han encontrado relojes",
|
||||
"Connectable": "Conectable",
|
||||
"No app has settings": "Ninguna aplicación tiene ajustes",
|
||||
|
@ -138,9 +138,9 @@
|
|||
"Reset": "Reiniciar",
|
||||
"Beep": "Bip",
|
||||
"System": "Sistema",
|
||||
"Locale": "Localidad",
|
||||
"Locale": "Configuración regional",
|
||||
"Message": "Mensaje",
|
||||
"Set Time": "Hora de la cita",
|
||||
"Set Time": "Definir la hora",
|
||||
"Vibrate": "Vibrar",
|
||||
"Alerts": "Alertas",
|
||||
"Timer": "Temporizador",
|
||||
|
@ -185,7 +185,7 @@
|
|||
"fuzzyw": {
|
||||
"//": "App-specific overrides",
|
||||
"*$1 o'clock": "*$1 en punto",
|
||||
"five past *$1": "*$1 y cincq",
|
||||
"five past *$1": "*$1 y cinco",
|
||||
"ten past *$1": "*$1 y diez",
|
||||
"quarter past *$1": "*$1 y cuarto",
|
||||
"twenty past *$1": "*$1 y veinte",
|
||||
|
@ -197,4 +197,4 @@
|
|||
"ten to *$2": "*$2 menos diez",
|
||||
"five to *$2": "*$2 menos cinco"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,5 +25,6 @@
|
|||
{"code":"sl_SL","name":"Slovenian","url":"sl_SL.json"},
|
||||
{"code":"nn_NO","name":"Norwegian (Nynorsk)","url":"nn_NO.json"},
|
||||
{"code":"hr_HR","name":"Croatian","url":"hr_HR.json"},
|
||||
{"code":"ja_JA","name":"Japanese (beta)","url":"ja_JA.json"}
|
||||
{"code":"ja_JA","name":"Japanese (beta)","url":"ja_JA.json"},
|
||||
{"code":"ca_ES","name":"Catalan","url":"ca_ES.json"}
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue