forked from FOSS/BangleApps
gpstrek - New app
parent
a456f4ff62
commit
f22c5c8e7f
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,43 @@
|
|||
# GPS Trekking
|
||||
|
||||
Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!
|
||||
|
||||
This app is inspired by and uses code from "GPS Navigation" and "Navigation compass".
|
||||
|
||||
## Usage
|
||||
|
||||
Tapping or button to switch to the next information display, swipe right for the menu.
|
||||
|
||||
Choose either a route or a waypoint as basis for the display.
|
||||
|
||||
After this selection and availability of a GPS fix the compass will show a blue dot for your destination and a green one for possibly available waypoints on the way.
|
||||
Waypoints are shown with name if available and distance to waypoint.
|
||||
|
||||
### Route
|
||||
|
||||
Routes can be created from .gpx files containing "trkpt" elements with this script: [createRoute.sh](createRoute.sh)
|
||||
|
||||
The resulting file needs to be uploaded to the watch and will be shown in the file selection menu.
|
||||
|
||||
The route can be mirrored to switch start and destination.
|
||||
|
||||
If the GPS position is closer than 30m to the next waypoint, the route is automatically advanced to the next waypoint.
|
||||
|
||||
### Waypoints
|
||||
|
||||
You can select a waypoint from the "Waypoints" app as destination.
|
||||
|
||||
## Calibration
|
||||
|
||||
### Altitude
|
||||
|
||||
You can correct the barometric altitude display either by manually setting a known correct value or using the GPS fix elevation as reference. This will only affect the display of altitude values.
|
||||
|
||||
### Compass
|
||||
|
||||
If the compass fallback starts to show unreliable values, you can reset the calibration in the menu. It starts to show values again after turning 360°.
|
||||
|
||||
## Widget
|
||||
|
||||
The widget keeps the sensors alive and records some very basic statics when the app is not started.
|
||||
This uses a lot of power so ensure to stop the app if you are not actively using it.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwIjggOAApMD4AFJg4FF8AFJh/wApMf/AFJn/8ApN//wFDvfeAof774FD+fPLwYFBMAUB8fHAoUDAoJaCgfD4YFIg+D4JgCAosPAoJgCh6DBAoUfAoJgCjwFBvAFBnwFBvgFBngFBngFBvh3BnwFBvH//8eMgQFBMwX//k//5eB//wh//wAFBAQcDRoU/4EDJQfAbYbfFACYA="))
|
|
@ -0,0 +1,828 @@
|
|||
const STORAGE = require("Storage");
|
||||
const showWidgets = true;
|
||||
let numberOfSlices=4;
|
||||
|
||||
if (showWidgets){
|
||||
Bangle.loadWidgets();
|
||||
}
|
||||
|
||||
let state = WIDGETS["gpstrek"].getState();
|
||||
WIDGETS["gpstrek"].start();
|
||||
|
||||
function parseNumber(toParse){
|
||||
if (toParse.includes(".")) return parseFloat(toParse);
|
||||
return parseFloat("" + toParse + ".0");
|
||||
}
|
||||
|
||||
function parseWaypoint(filename, offset, result){
|
||||
result.lat = parseNumber(STORAGE.read(filename, offset, 11));
|
||||
result.lon = parseNumber(STORAGE.read(filename, offset += 11, 12));
|
||||
return offset + 12;
|
||||
}
|
||||
|
||||
function parseWaypointWithElevation(filename, offset, result){
|
||||
offset = parseWaypoint(filename, offset, result);
|
||||
result.alt = parseNumber(STORAGE.read(filename, offset, 6));
|
||||
return offset + 6;
|
||||
}
|
||||
|
||||
function parseWaypointWithName(filename, offset, result){
|
||||
offset = parseWaypoint(filename, offset, result);
|
||||
return parseName(filename, offset, result);
|
||||
}
|
||||
|
||||
function parseName(filename, offset, result){
|
||||
let nameLength = STORAGE.read(filename, offset, 2) - 0;
|
||||
result.name = STORAGE.read(filename, offset += 2, nameLength);
|
||||
return offset + nameLength;
|
||||
}
|
||||
|
||||
function parseWaypointWithElevationAndName(filename, offset, result){
|
||||
offset = parseWaypointWithElevation(filename, offset, result);
|
||||
return parseName(filename, offset, result);
|
||||
}
|
||||
|
||||
function getEntry(filename, offset, result){
|
||||
result.fileOffset = offset;
|
||||
let type = STORAGE.read(filename, offset++, 1);
|
||||
if (type == "") return -1;
|
||||
switch (type){
|
||||
case "A":
|
||||
offset = parseWaypoint(filename, offset, result);
|
||||
break;
|
||||
case "B":
|
||||
offset = parseWaypointWithName(filename, offset, result);
|
||||
break;
|
||||
case "C":
|
||||
offset = parseWaypointWithElevation(filename, offset, result);
|
||||
break;
|
||||
case "D":
|
||||
offset = parseWaypointWithElevationAndName(filename, offset, result);
|
||||
break;
|
||||
default:
|
||||
print("Unknown entry type", type);
|
||||
return -1;
|
||||
}
|
||||
offset++;
|
||||
|
||||
result.fileLength = offset - result.fileOffset;
|
||||
//print(result);
|
||||
return offset;
|
||||
}
|
||||
|
||||
const labels = ["N","NE","E","SE","S","SW","W","NW"];
|
||||
const loc = require("locale");
|
||||
|
||||
function matchFontSize(graphics, text, height, width){
|
||||
graphics.setFontVector(height);
|
||||
let metrics;
|
||||
let size = 1;
|
||||
while (graphics.stringMetrics(text).width > 0.90 * width){
|
||||
size -= 0.05;
|
||||
graphics.setFont("Vector",Math.floor(height*size));
|
||||
}
|
||||
}
|
||||
|
||||
function getDoubleLineSlice(title1,title2,provider1,provider2,refreshTime){
|
||||
let lastDrawn = Date.now() - Math.random()*refreshTime;
|
||||
return {
|
||||
refresh: function (){
|
||||
return Date.now() - lastDrawn > (Bangle.isLocked()?(refreshTime?refreshTime:5000):(refreshTime?refreshTime*2:10000));
|
||||
},
|
||||
draw: function (graphics, x, y, height, width){
|
||||
lastDrawn = Date.now();
|
||||
if (typeof title1 == "function") title1 = title1();
|
||||
if (typeof title2 == "function") title2 = title2();
|
||||
graphics.clearRect(x,y,x+width,y+height);
|
||||
|
||||
let value = provider1();
|
||||
matchFontSize(graphics, title1 + value, Math.floor(height*0.5), width);
|
||||
graphics.setFontAlign(-1,-1);
|
||||
graphics.drawString(title1, x+2, y);
|
||||
graphics.setFontAlign(1,-1);
|
||||
graphics.drawString(value, x+width, y);
|
||||
|
||||
value = provider2();
|
||||
matchFontSize(graphics, title2 + value, Math.floor(height*0.5), width);
|
||||
graphics.setFontAlign(-1,-1);
|
||||
graphics.drawString(title2, x+2, y+(height*0.5));
|
||||
graphics.setFontAlign(1,-1);
|
||||
graphics.drawString(value, x+width, y+(height*0.5));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getTargetSlice(targetDataSource){
|
||||
let nameIndex = 0;
|
||||
let lastDrawn = Date.now() - Math.random()*3000;
|
||||
return {
|
||||
refresh: function (){
|
||||
return Date.now() - lastDrawn > (Bangle.isLocked()?10000:3000);
|
||||
},
|
||||
draw: function (graphics, x, y, height, width){
|
||||
lastDrawn = Date.now();
|
||||
graphics.clearRect(x,y,x+width,y+height);
|
||||
if (targetDataSource.icon){
|
||||
graphics.drawImage(targetDataSource.icon,x,y + (height - 16)/2);
|
||||
x += 16;
|
||||
width -= 16;
|
||||
}
|
||||
|
||||
if (!targetDataSource.getTarget() || !targetDataSource.getStart()) return;
|
||||
|
||||
let dist = distance(targetDataSource.getStart(),targetDataSource.getTarget());
|
||||
if (isNaN(dist)) dist = Infinity;
|
||||
let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°";
|
||||
if (targetDataSource.getTarget().name) {
|
||||
graphics.setFont("Vector",Math.floor(height*0.5));
|
||||
let scrolledName = (targetDataSource.getTarget().name || "").substring(nameIndex);
|
||||
if (graphics.stringMetrics(scrolledName).width > width){
|
||||
nameIndex++;
|
||||
} else {
|
||||
nameIndex = 0;
|
||||
}
|
||||
graphics.drawString(scrolledName, x+2, y);
|
||||
|
||||
let distanceString = loc.distance(dist,2);
|
||||
matchFontSize(graphics, distanceString + bearingString, height*0.5, width);
|
||||
graphics.drawString(bearingString, x+2, y+(height*0.5));
|
||||
graphics.setFontAlign(1,-1);
|
||||
graphics.drawString(distanceString, x + width, y+(height*0.5));
|
||||
} else {
|
||||
graphics.setFont("Vector",Math.floor(height*1));
|
||||
let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°";
|
||||
let formattedDist = loc.distance(dist,2);
|
||||
let distNum = (formattedDist.match(/[0-9\.]+/) || [Infinity])[0];
|
||||
let size = 0.8;
|
||||
let distNumMetrics;
|
||||
while (graphics.stringMetrics(bearingString).width + (distNumMetrics = graphics.stringMetrics(distNum)).width > 0.90 * width){
|
||||
size -= 0.05;
|
||||
graphics.setFont("Vector",Math.floor(height*size));
|
||||
}
|
||||
graphics.drawString(bearingString, x+2, y + (height - distNumMetrics.height)/2);
|
||||
graphics.setFontAlign(1,-1);
|
||||
graphics.drawString(distNum, x + width, y + (height - distNumMetrics.height)/2);
|
||||
graphics.setFont("Vector",Math.floor(height*0.25));
|
||||
|
||||
graphics.setFontAlign(-1,1);
|
||||
if (targetDataSource.getProgress){
|
||||
graphics.drawString(targetDataSource.getProgress(), x + 2, y + height);
|
||||
}
|
||||
graphics.setFontAlign(1,1);
|
||||
if (!isNaN(distNum) && distNum != Infinity)
|
||||
graphics.drawString(formattedDist.match(/[a-zA-Z]+/), x + width, y + height);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function drawCompass(graphics, x, y, height, width, increment, start){
|
||||
graphics.setFont12x20();
|
||||
graphics.setFontAlign(0,-1);
|
||||
graphics.setColor(graphics.theme.fg);
|
||||
let frag = 0 - start%15;
|
||||
if (frag>0) frag = 0;
|
||||
let xpos = 0 + frag*increment;
|
||||
for (let i=start;i<=720;i+=15){
|
||||
var res = i + frag;
|
||||
if (res%90==0) {
|
||||
graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2);
|
||||
graphics.fillRect(xpos-2,Math.floor(y+height*0.6),xpos+2,Math.floor(y+height));
|
||||
} else if (res%45==0) {
|
||||
graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2);
|
||||
graphics.fillRect(xpos-2,Math.floor(y+height*0.75),xpos+2,Math.floor(y+height));
|
||||
} else if (res%15==0) {
|
||||
graphics.fillRect(xpos,Math.floor(y+height*0.9),xpos+1,Math.floor(y+height));
|
||||
}
|
||||
xpos+=increment*15;
|
||||
if (xpos > width + 20) break;
|
||||
}
|
||||
}
|
||||
|
||||
function getCompassSlice(compassDataSource){
|
||||
let lastDrawn = Date.now() - Math.random()*2000;
|
||||
const buffers = 4;
|
||||
let buf = [];
|
||||
return {
|
||||
refresh : function (){return Bangle.isLocked()?(Date.now() - lastDrawn > 2000):true;},
|
||||
draw: function (graphics, x,y,height,width){
|
||||
lastDrawn = Date.now();
|
||||
const max = 180;
|
||||
const increment=width/max;
|
||||
|
||||
graphics.clearRect(x,y,x+width,y+height);
|
||||
|
||||
var start = compassDataSource.getCourse() - 90;
|
||||
if (isNaN(compassDataSource.getCourse())) start = -90;
|
||||
if (start<0) start+=360;
|
||||
start = start % 360;
|
||||
|
||||
if (state.acc && compassDataSource.getCourseType() == "MAG"){
|
||||
drawCompass(graphics,0,y+width*0.05,height-width*0.05,width,increment,start);
|
||||
} else {
|
||||
drawCompass(graphics,0,y,height,width,increment,start);
|
||||
}
|
||||
|
||||
|
||||
if (compassDataSource.getPoints){
|
||||
for (let p of compassDataSource.getPoints()){
|
||||
var bpos = p.bearing - compassDataSource.getCourse();
|
||||
if (bpos>180) bpos -=360;
|
||||
if (bpos<-180) bpos +=360;
|
||||
bpos+=120;
|
||||
let min = 0;
|
||||
let max = 180;
|
||||
if (bpos<=min){
|
||||
bpos = Math.floor(width*0.05);
|
||||
} else if (bpos>=max) {
|
||||
bpos = Math.ceil(width*0.95);
|
||||
} else {
|
||||
bpos=Math.round(bpos*increment);
|
||||
}
|
||||
graphics.setColor(p.color);
|
||||
graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03));
|
||||
}
|
||||
}
|
||||
if (compassDataSource.getMarkers){
|
||||
for (let m of compassDataSource.getMarkers()){
|
||||
g.setColor(m.fillcolor);
|
||||
let mpos = m.xpos * width;
|
||||
if (m.xpos < 0.05) mpos = Math.floor(width*0.05);
|
||||
if (m.xpos > 0.95) mpos = Math.ceil(width*0.95);
|
||||
g.fillPoly(triangle(mpos,y+height-m.height, m.height, m.width));
|
||||
g.setColor(m.linecolor);
|
||||
g.drawPoly(triangle(mpos,y+height-m.height, m.height, m.width),true);
|
||||
}
|
||||
}
|
||||
graphics.setColor(g.theme.fg);
|
||||
graphics.fillRect(x,y,Math.floor(width*0.05),y+height);
|
||||
graphics.fillRect(Math.ceil(width*0.95),y,width,y+height);
|
||||
if (state.acc && compassDataSource.getCourseType() == "MAG") {
|
||||
let xh = E.clip(width*0.5-height/2+(((state.acc.x+1)/2)*height),width*0.5 - height/2, width*0.5 + height/2);
|
||||
let yh = E.clip(y+(((state.acc.y+1)/2)*height),y,y+height);
|
||||
|
||||
graphics.fillRect(width*0.5 - height/2, y, width*0.5 + height/2, y + Math.floor(width*0.05));
|
||||
|
||||
graphics.setColor(g.theme.bg);
|
||||
graphics.drawLine(width*0.5 - 5, y, width*0.5 - 5, y + Math.floor(width*0.05));
|
||||
graphics.drawLine(width*0.5 + 5, y, width*0.5 + 5, y + Math.floor(width*0.05));
|
||||
graphics.fillRect(xh-1,y,xh+1,y+Math.floor(width*0.05));
|
||||
|
||||
let left = Math.floor(width*0.05);
|
||||
let right = Math.ceil(width*0.95);
|
||||
graphics.drawLine(0,y+height/2-5,left,y+height/2-5);
|
||||
graphics.drawLine(right,y+height/2-5,x+width,y+height/2-5);
|
||||
graphics.drawLine(0,y+height/2+5,left,y+height/2+5);
|
||||
graphics.drawLine(right,y+height/2+5,x+width,y+height/2+5);
|
||||
graphics.fillRect(0,yh-1,left,yh+1);
|
||||
graphics.fillRect(right,yh-1,x+width,yh+1);
|
||||
}
|
||||
graphics.setColor(g.theme.fg);
|
||||
graphics.drawRect(Math.floor(width*0.05),y,Math.ceil(width*0.95),y+height);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function radians(a) {
|
||||
return a*Math.PI/180;
|
||||
}
|
||||
|
||||
function degrees(a) {
|
||||
var d = a*180/Math.PI;
|
||||
return (d+360)%360;
|
||||
}
|
||||
|
||||
function bearing(a,b){
|
||||
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
|
||||
var delta = radians(b.lon-a.lon);
|
||||
var alat = radians(a.lat);
|
||||
var blat = radians(b.lat);
|
||||
var y = Math.sin(delta) * Math.cos(blat);
|
||||
var x = Math.cos(alat)*Math.sin(blat) -
|
||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||
return Math.round(degrees(Math.atan2(y, x)));
|
||||
}
|
||||
|
||||
function distance(a,b){
|
||||
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
|
||||
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||
var y = radians(b.lat-a.lat);
|
||||
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
|
||||
}
|
||||
|
||||
function triangle (x, y, width, height){
|
||||
return [
|
||||
Math.round(x),Math.round(y),
|
||||
Math.round(x+width * 0.5), Math.round(y+height),
|
||||
Math.round(x-width * 0.5), Math.round(y+height)
|
||||
];
|
||||
}
|
||||
|
||||
function setButtons(){
|
||||
Bangle.setUI("leftright", (dir)=>{
|
||||
if (dir < 0) {
|
||||
nextScreen();
|
||||
} else if (dir > 0) {
|
||||
switchMenu();
|
||||
} else {
|
||||
nextScreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getApproxFileSize(name){
|
||||
let currentStart = STORAGE.getStats().totalBytes;
|
||||
let currentSize = 0;
|
||||
for (let i = currentStart; i > 500; i/=2){
|
||||
let currentDiff = i;
|
||||
//print("Searching", currentDiff);
|
||||
while (STORAGE.read(name, currentSize+currentDiff, 1) == ""){
|
||||
//print("Loop", currentDiff);
|
||||
currentDiff = Math.ceil(currentDiff/2);
|
||||
}
|
||||
i = currentDiff*2;
|
||||
currentSize += currentDiff;
|
||||
}
|
||||
return currentSize;
|
||||
}
|
||||
|
||||
function parseRouteData(filename, progressMonitor){
|
||||
let routeInfo = {};
|
||||
|
||||
routeInfo.filename = filename;
|
||||
routeInfo.refs = [];
|
||||
|
||||
let c = {};
|
||||
let scanOffset = 0;
|
||||
routeInfo.length = 0;
|
||||
routeInfo.count = 0;
|
||||
routeInfo.mirror = false;
|
||||
let lastSeenWaypoint;
|
||||
let lastSeenAlt;
|
||||
let waypoint = {};
|
||||
|
||||
routeInfo.up = 0;
|
||||
routeInfo.down = 0;
|
||||
|
||||
let size = getApproxFileSize(filename);
|
||||
|
||||
while ((scanOffset = getEntry(filename, scanOffset, waypoint)) > 0) {
|
||||
if (routeInfo.count % 5 == 0) progressMonitor(scanOffset, "Loading", size);
|
||||
if (lastSeenWaypoint){
|
||||
routeInfo.length += distance(lastSeenWaypoint, waypoint);
|
||||
|
||||
let diff = waypoint.alt - lastSeenAlt;
|
||||
//print("Distance", routeInfo.length, "alt", lastSeenAlt, waypoint.alt, diff);
|
||||
if (waypoint.alt && lastSeenAlt && diff > 3){
|
||||
if (lastSeenAlt < waypoint.alt){
|
||||
//print("Up", diff);
|
||||
routeInfo.up += diff;
|
||||
} else {
|
||||
//print("Down", diff);
|
||||
routeInfo.down += diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
routeInfo.count++;
|
||||
routeInfo.refs.push(waypoint.fileOffset);
|
||||
lastSeenWaypoint = waypoint;
|
||||
if (!isNaN(waypoint.alt)) lastSeenAlt = waypoint.alt;
|
||||
waypoint = {};
|
||||
}
|
||||
|
||||
set(routeInfo, 0);
|
||||
return routeInfo;
|
||||
}
|
||||
|
||||
function hasPrev(route){
|
||||
if (route.mirror) return route.index < (route.count - 1);
|
||||
return route.index > 0;
|
||||
}
|
||||
|
||||
function hasNext(route){
|
||||
if (route.mirror) return route.index > 0;
|
||||
return route.index < (route.count - 1);
|
||||
}
|
||||
|
||||
function next(route){
|
||||
if (!hasNext(route)) return;
|
||||
if (route.mirror) set(route, --route.index);
|
||||
if (!route.mirror) set(route, ++route.index);
|
||||
}
|
||||
|
||||
function set(route, index){
|
||||
route.currentWaypoint = {};
|
||||
route.index = index;
|
||||
getEntry(route.filename, route.refs[index], route.currentWaypoint);
|
||||
}
|
||||
|
||||
function prev(route){
|
||||
if (!hasPrev(route)) return;
|
||||
if (route.mirror) set(route, ++route.index);
|
||||
if (!route.mirror) set(route, --route.index);
|
||||
}
|
||||
|
||||
let lastMirror;
|
||||
let cachedLast;
|
||||
|
||||
function getLast(route){
|
||||
let wp = {};
|
||||
if (lastMirror != route.mirror){
|
||||
if (route.mirror) getEntry(route.filename, route.refs[0], wp);
|
||||
if (!route.mirror) getEntry(route.filename, route.refs[route.count - 1], wp);
|
||||
lastMirror = route.mirror;
|
||||
cachedLast = wp;
|
||||
}
|
||||
return cachedLast;
|
||||
}
|
||||
|
||||
function removeMenu(){
|
||||
E.showMenu();
|
||||
switchNav();
|
||||
}
|
||||
|
||||
function showProgress(progress, title, max){
|
||||
//print("Progress",progress,max)
|
||||
let message = title? title: "Loading";
|
||||
if (max){
|
||||
message += " " + E.clip((progress/max*100),0,100).toFixed(0) +"%";
|
||||
} else {
|
||||
let dots = progress % 4;
|
||||
for (let i = 0; i < dots; i++) message += ".";
|
||||
for (let i = dots; i < 4; i++) message += " ";
|
||||
}
|
||||
E.showMessage(message);
|
||||
}
|
||||
|
||||
function handleLoading(c){
|
||||
E.showMenu();
|
||||
state.route = parseRouteData(c, showProgress);
|
||||
state.waypoint = null;
|
||||
removeMenu();
|
||||
state.route.mirror = false;
|
||||
}
|
||||
|
||||
function showRouteSelector (){
|
||||
var menu = {
|
||||
"" : {
|
||||
back : showRouteMenu,
|
||||
}
|
||||
};
|
||||
|
||||
for (let c of STORAGE.list((/\.trf$/))){
|
||||
let file = c;
|
||||
menu[file] = ()=>{handleLoading(file);};
|
||||
}
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showRouteMenu(){
|
||||
var menu = {
|
||||
"" : {
|
||||
"title" : "Route",
|
||||
back : showMenu,
|
||||
},
|
||||
"Select file" : showRouteSelector
|
||||
};
|
||||
|
||||
if (state.route){
|
||||
menu.Mirror = {
|
||||
value: state && state.route && !!state.route.mirror || false,
|
||||
onchange: v=>{
|
||||
state.route.mirror = v;
|
||||
}
|
||||
};
|
||||
menu['Select closest waypoint'] = function () {
|
||||
if (state.currentPos && state.currentPos.lat){
|
||||
setClosestWaypoint(state.route, null, showProgress); removeMenu();
|
||||
} else {
|
||||
E.showAlert("No position").then(()=>{E.showMenu(menu);});
|
||||
}
|
||||
};
|
||||
menu['Select closest waypoint (not visited)'] = function () {
|
||||
if (state.currentPos && state.currentPos.lat){
|
||||
setClosestWaypoint(state.route, state.route.index, showProgress); removeMenu();
|
||||
} else {
|
||||
E.showAlert("No position").then(()=>{E.showMenu(menu);});
|
||||
}
|
||||
};
|
||||
menu['Select waypoint'] = {
|
||||
value : state.route.index,
|
||||
min:1,max:state.route.count,step:1,
|
||||
onchange : v => { set(state.route, v-1); }
|
||||
};
|
||||
menu['Select waypoint as current position'] = function (){
|
||||
state.currentPos.lat = state.route.currentWaypoint.lat;
|
||||
state.currentPos.lon = state.route.currentWaypoint.lon;
|
||||
state.currentPos.alt = state.route.currentWaypoint.alt;
|
||||
removeMenu();
|
||||
};
|
||||
}
|
||||
|
||||
if (state.route && hasPrev(state.route))
|
||||
menu['Previous waypoint'] = function() { prev(state.route); removeMenu(); };
|
||||
if (state.route && hasNext(state.route))
|
||||
menu['Next waypoint'] = function() { next(state.route); removeMenu(); };
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showWaypointSelector(){
|
||||
let waypoints = require("waypoints").load();
|
||||
var menu = {
|
||||
"" : {
|
||||
back : showWaypointMenu,
|
||||
}
|
||||
};
|
||||
|
||||
for (let c in waypoints){
|
||||
menu[waypoints[c].name] = function (){
|
||||
state.waypoint = waypoints[c];
|
||||
state.waypointIndex = c;
|
||||
state.route = null;
|
||||
removeMenu();
|
||||
};
|
||||
}
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showCalibrationMenu(){
|
||||
let menu = {
|
||||
"" : {
|
||||
"title" : "Calibration",
|
||||
back : showMenu,
|
||||
},
|
||||
"Barometer (GPS)" : ()=>{
|
||||
if (!state.currentPos || isNaN(state.currentPos.alt)){
|
||||
E.showAlert("No GPS altitude").then(()=>{E.showMenu(menu);});
|
||||
} else {
|
||||
state.calibAltDiff = state.altitude - state.currentPos.alt;
|
||||
E.showAlert("Calibrated Altitude Difference: " + state.calibAltDiff.toFixed(0)).then(()=>{removeMenu();});
|
||||
}
|
||||
},
|
||||
"Barometer (Manual)" : {
|
||||
value : Math.round(state.currentPos && (state.currentPos.alt != undefined && !isNaN(state.currentPos.alt)) ? state.currentPos.alt: state.altitude),
|
||||
min:-2000,max: 10000,step:1,
|
||||
onchange : v => { state.calibAltDiff = state.altitude - v; }
|
||||
},
|
||||
"Reset Compass" : ()=>{ Bangle.resetCompass(); removeMenu();},
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showWaypointMenu(){
|
||||
let menu = {
|
||||
"" : {
|
||||
"title" : "Waypoint",
|
||||
back : showMenu,
|
||||
},
|
||||
"Select waypoint" : showWaypointSelector,
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showMenu(){
|
||||
var mainmenu = {
|
||||
"" : {
|
||||
"title" : "Main",
|
||||
back : removeMenu,
|
||||
},
|
||||
"Route" : showRouteMenu,
|
||||
"Waypoint" : showWaypointMenu,
|
||||
"Calibration": showCalibrationMenu,
|
||||
"Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {state.active = true; removeMenu();} else {E.showMenu(mainmenu);}});},
|
||||
"Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS["gpstrek"].stop(); removeMenu();} else {E.showMenu(mainmenu);}});},
|
||||
"Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS["gpstrek"].resetState(); removeMenu();} else {E.showMenu(mainmenu);}});},
|
||||
"Slices" : {
|
||||
value : numberOfSlices,
|
||||
min:1,max:6,step:1,
|
||||
onchange : v => { setNumberOfSlices(v); }
|
||||
},
|
||||
};
|
||||
|
||||
E.showMenu(mainmenu);
|
||||
}
|
||||
|
||||
let scheduleDraw = true;
|
||||
|
||||
function switchMenu(){
|
||||
screen = 0;
|
||||
scheduleDraw = false;
|
||||
showMenu();
|
||||
}
|
||||
|
||||
function drawInTimeout(){
|
||||
setTimeout(()=>{
|
||||
draw();
|
||||
if (scheduleDraw)
|
||||
setTimeout(drawInTimeout, 0);
|
||||
},0);
|
||||
}
|
||||
|
||||
function switchNav(){
|
||||
if (!screen) screen = 1;
|
||||
setButtons();
|
||||
scheduleDraw = true;
|
||||
drawInTimeout();
|
||||
}
|
||||
|
||||
function nextScreen(){
|
||||
screen++;
|
||||
if (screen > maxScreens){
|
||||
screen = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function setClosestWaypoint(route, startindex, progress){
|
||||
if (startindex >= state.route.count) startindex = state.route.count - 1;
|
||||
if (!state.currentPos.lat){
|
||||
set(route, startindex);
|
||||
return;
|
||||
}
|
||||
let minDist = 100000000000000;
|
||||
let minIndex = 0;
|
||||
for (let i = startindex?startindex:0; i < route.count - 1; i++){
|
||||
if (progress && (i % 5 == 0)) progress(i-(startindex?startindex:0), "Searching", route.count);
|
||||
let wp = {};
|
||||
getEntry(route.filename, route.refs[i], wp);
|
||||
let curDist = distance(state.currentPos, wp);
|
||||
if (curDist < minDist){
|
||||
minDist = curDist;
|
||||
minIndex = i;
|
||||
} else {
|
||||
if (startindex) break;
|
||||
}
|
||||
}
|
||||
set(route, minIndex);
|
||||
}
|
||||
|
||||
let screen = 1;
|
||||
|
||||
const compassSliceData = {
|
||||
getCourseType: function(){
|
||||
return (state.currentPos && state.currentPos.course) ? "GPS" : "MAG";
|
||||
},
|
||||
getCourse: function (){
|
||||
if(compassSliceData.getCourseType() == "GPS") return state.currentPos.course;
|
||||
return state.compassHeading?state.compassHeading:undefined;
|
||||
},
|
||||
getPoints: function (){
|
||||
let points = [];
|
||||
if (state.currentPos && state.currentPos.lon && state.route && state.route.currentWaypoint){
|
||||
points.push({bearing:bearing(state.currentPos, state.route.currentWaypoint), color:"#0f0"});
|
||||
}
|
||||
if (state.currentPos && state.currentPos.lon && state.route){
|
||||
points.push({bearing:bearing(state.currentPos, getLast(state.route)), color:"#00f"});
|
||||
}
|
||||
return points;
|
||||
},
|
||||
getMarkers: function (){
|
||||
return [{xpos:0.5, width:10, height:10, linecolor:g.theme.fg, fillcolor:"#f00"}];
|
||||
}
|
||||
};
|
||||
|
||||
const waypointData = {
|
||||
icon: atob("EBCBAAAAAAAAAAAAcIB+zg/uAe4AwACAAAAAAAAAAAAAAAAA"),
|
||||
getProgress: function() {
|
||||
return (state.route.index + 1) + "/" + state.route.count;
|
||||
},
|
||||
getTarget: function (){
|
||||
if (distance(state.currentPos,state.route.currentWaypoint) < 30 && hasNext(state.route)){
|
||||
next(state.route);
|
||||
Bangle.buzz(1000);
|
||||
}
|
||||
return state.route.currentWaypoint;
|
||||
},
|
||||
getStart: function (){
|
||||
return state.currentPos;
|
||||
}
|
||||
};
|
||||
|
||||
const finishData = {
|
||||
icon: atob("EBABAAA/4DmgJmAmYDmgOaAmYD/gMAAwADAAMAAwAAAAAAA="),
|
||||
getTarget: function (){
|
||||
if (state.route) return getLast(state.route);
|
||||
if (state.waypoint) return state.waypoint;
|
||||
},
|
||||
getStart: function (){
|
||||
return state.currentPos;
|
||||
}
|
||||
};
|
||||
|
||||
let sliceHeight;
|
||||
function setNumberOfSlices(number){
|
||||
numberOfSlices = number;
|
||||
sliceHeight = Math.floor((g.getHeight()-(showWidgets?24:0))/numberOfSlices);
|
||||
}
|
||||
|
||||
let slices = [];
|
||||
let maxScreens = 1;
|
||||
setNumberOfSlices(3);
|
||||
|
||||
let compassSlice = getCompassSlice(compassSliceData);
|
||||
let waypointSlice = getTargetSlice(waypointData);
|
||||
let finishSlice = getTargetSlice(finishData);
|
||||
let eleSlice = getDoubleLineSlice("Up","Down",()=>{
|
||||
return loc.distance(state.up,3) + "/" + (state.route ? loc.distance(state.route.up,3):"---");
|
||||
},()=>{
|
||||
return loc.distance(state.down,3) + "/" + (state.route ? loc.distance(state.route.down,3): "---");
|
||||
});
|
||||
|
||||
let statusSlice = getDoubleLineSlice("Speed","Alt",()=>{
|
||||
let speed = 0;
|
||||
if (state.currentPos && state.currentPos.speed) speed = state.currentPos.speed;
|
||||
return loc.speed(speed,2);
|
||||
},()=>{
|
||||
let alt = Infinity;
|
||||
if (!isNaN(state.altitude)){
|
||||
alt = isNaN(state.calibAltDiff) ? state.altitude : (state.altitude - state.calibAltDiff);
|
||||
}
|
||||
if (state.currentPos && state.currentPos.alt) alt = state.currentPos.alt;
|
||||
return loc.distance(alt,3);
|
||||
});
|
||||
|
||||
let status2Slice = getDoubleLineSlice("Compass","GPS",()=>{
|
||||
return (state.compassHeading?Math.round(state.compassHeading):"---") + "°";
|
||||
},()=>{
|
||||
let course = "---°";
|
||||
if (state.currentPos && state.currentPos.course) course = state.currentPos.course + "°";
|
||||
return course;
|
||||
},200);
|
||||
|
||||
let healthSlice = getDoubleLineSlice("Heart","Steps",()=>{
|
||||
return state.bpm;
|
||||
},()=>{
|
||||
return state.steps;
|
||||
});
|
||||
|
||||
let system2Slice = getDoubleLineSlice("Bat","",()=>{
|
||||
return (Bangle.isCharging()?"+":"") + E.getBattery().toFixed(0)+"% " + NRF.getBattery().toFixed(2) + "V";
|
||||
},()=>{
|
||||
return "";
|
||||
});
|
||||
|
||||
let systemSlice = getDoubleLineSlice("RAM","Storage",()=>{
|
||||
let ram = process.memory(false);
|
||||
return ((ram.blocksize * ram.free)/1024).toFixed(0)+"kB";
|
||||
},()=>{
|
||||
return (STORAGE.getFree()/1024).toFixed(0)+"kB";
|
||||
});
|
||||
|
||||
function updateSlices(){
|
||||
slices = [];
|
||||
slices.push(compassSlice);
|
||||
|
||||
if (state.currentPos && state.currentPos.lat && state.route && state.route.currentWaypoint && state.route.index < state.route.count - 1) {
|
||||
slices.push(waypointSlice);
|
||||
}
|
||||
if (state.currentPos && state.currentPos.lat && (state.route || state.waypoint)) {
|
||||
slices.push(finishSlice);
|
||||
}
|
||||
if ((state.route && state.route.down !== undefined) || state.down != undefined) {
|
||||
slices.push(eleSlice);
|
||||
}
|
||||
slices.push(statusSlice);
|
||||
slices.push(status2Slice);
|
||||
slices.push(healthSlice);
|
||||
slices.push(systemSlice);
|
||||
slices.push(system2Slice);
|
||||
maxScreens = Math.ceil(slices.length/numberOfSlices);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
g.clearRect(0,(showWidgets ? 24 : 0), g.getWidth(),g.getHeight());
|
||||
}
|
||||
let lastDrawnScreen;
|
||||
let firstDraw = true;
|
||||
|
||||
function draw(){
|
||||
if (!screen) return;
|
||||
let ypos = showWidgets ? 24 : 0;
|
||||
|
||||
let firstSlice = (screen-1)*numberOfSlices;
|
||||
|
||||
updateSlices();
|
||||
|
||||
let force = lastDrawnScreen != screen || firstDraw;
|
||||
if (force){
|
||||
clear();
|
||||
if (showWidgets){
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
}
|
||||
lastDrawnScreen = screen;
|
||||
|
||||
for (let slice of slices.slice(firstSlice,firstSlice + numberOfSlices)) {
|
||||
g.reset();
|
||||
if (!slice.refresh || slice.refresh() || force) slice.draw(g,0,ypos,sliceHeight,g.getWidth());
|
||||
ypos += sliceHeight+1;
|
||||
g.drawLine(0,ypos-1,g.getWidth(),ypos-1);
|
||||
}
|
||||
firstDraw = false;
|
||||
}
|
||||
|
||||
|
||||
switchNav();
|
||||
|
||||
g.clear();
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
[ -z "$1" ] && echo Give gpx file name
|
||||
|
||||
|
||||
xmlstarlet select -t -m '//_:trkpt' \
|
||||
--if '_:name and _:ele' -o D \
|
||||
--elif '_:ele and not(_:name)' -o C \
|
||||
--elif 'not(_:ele) and _:name' -o B \
|
||||
--else -o A -b \
|
||||
-v 'format-number(@lat,"+00.0000000;-00.0000000")' \
|
||||
-v 'format-number(@lon,"+000.0000000;-000.0000000")' \
|
||||
--if '_:ele' -v 'format-number(_:ele,"+00000;-00000")' -b \
|
||||
--if _:name -v 'format-number(string-length(_:name),"00")' -v '_:name' -b \
|
||||
-n "$1" | iconv -f utf8 -t iso8859-1 > "$(basename "$1" | sed -e "s|.gpx||").trf"
|
Binary file not shown.
After Width: | Height: | Size: 851 B |
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "gpstrek",
|
||||
"name": "GPS Trekking",
|
||||
"version": "0.01",
|
||||
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
|
||||
"icon": "icon.png",
|
||||
"screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}],
|
||||
"tags": "tool,outdoors,gps",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"dependencies" : { "waypoints":"type" },
|
||||
"storage": [
|
||||
{"name":"gpstrek.app.js","url":"app.js"},
|
||||
{"name":"gpstrek.wid.js","url":"widget.js"},
|
||||
{"name":"gpstrek.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,129 @@
|
|||
(() => {
|
||||
const STORAGE=require('Storage');
|
||||
let state = STORAGE.readJSON("gpstrek.state.json")||{};
|
||||
|
||||
function saveState(){
|
||||
state.saved = Date.now();
|
||||
STORAGE.writeJSON("gpstrek.state.json", state);
|
||||
}
|
||||
|
||||
E.on("kill",()=>{
|
||||
if (state.active){
|
||||
saveState();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function onPulse(e){
|
||||
state.bpm = e.bpm;
|
||||
}
|
||||
|
||||
function onGPS(fix) {
|
||||
if(fix.fix) state.currentPos = fix;
|
||||
}
|
||||
|
||||
Bangle.on('accel', function(e) {
|
||||
state.acc = e;
|
||||
});
|
||||
|
||||
function onMag(e) {
|
||||
if (!state.compassHeading) state.compassHeading = e.heading;
|
||||
|
||||
//if (a+180)mod 360 == b then
|
||||
//return (a+b)/2 mod 360 and ((a+b)/2 mod 360) + 180 (they are both the solution, so you may choose one depending if you prefer counterclockwise or clockwise direction)
|
||||
//else
|
||||
//return arctan( (sin(a)+sin(b)) / (cos(a)+cos(b) )
|
||||
|
||||
/*
|
||||
let average;
|
||||
let a = radians(compassHeading);
|
||||
let b = radians(e.heading);
|
||||
if ((a+180) % 360 == b){
|
||||
average = ((a+b)/2 % 360); //can add 180 depending on rotation
|
||||
} else {
|
||||
average = Math.atan( (Math.sin(a)+Math.sin(b))/(Math.cos(a)+Math.cos(b)) );
|
||||
}
|
||||
print("Angle",compassHeading,e.heading, average);
|
||||
compassHeading = (compassHeading + degrees(average)) % 360;
|
||||
*/
|
||||
state.compassHeading = Math.round(e.heading);
|
||||
}
|
||||
|
||||
function onStep(e) {
|
||||
state.steps++;
|
||||
}
|
||||
|
||||
function onPressure(e) {
|
||||
state.pressure = e.pressure;
|
||||
|
||||
if (!state.altitude){
|
||||
state.altitude = e.altitude;
|
||||
state.up = 0;
|
||||
state.down = 0;
|
||||
}
|
||||
let diff = state.altitude - e.altitude;
|
||||
if (Math.abs(diff) > 3){
|
||||
if (diff > 0){
|
||||
state.up += diff;
|
||||
} else {
|
||||
state.down -= diff;
|
||||
}
|
||||
state.altitude = e.altitude;
|
||||
}
|
||||
}
|
||||
|
||||
function start(){
|
||||
Bangle.on('GPS', onGPS);
|
||||
Bangle.on("HRM", onPulse);
|
||||
Bangle.on("mag", onMag);
|
||||
Bangle.on("step", onStep);
|
||||
Bangle.on("pressure", onPressure);
|
||||
|
||||
Bangle.setGPSPower(1, "gpstrek");
|
||||
Bangle.setHRMPower(1, "gpstrek");
|
||||
Bangle.setCompassPower(1, "gpstrek");
|
||||
Bangle.setBarometerPower(1, "gpstrek");
|
||||
state.active = true;
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
function stop(){
|
||||
state.active = false;
|
||||
saveState();
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
function initState(){
|
||||
//cleanup volatile state here
|
||||
state.currentPos={};
|
||||
state.steps = Bangle.getStepCount();
|
||||
state.calibAltDiff = 0;
|
||||
state.up = 0;
|
||||
state.down = 0;
|
||||
}
|
||||
|
||||
if (state.saved && state.saved < Date.now() - 60000){
|
||||
initState();
|
||||
}
|
||||
|
||||
if (state.active){
|
||||
start();
|
||||
}
|
||||
|
||||
WIDGETS["gpstrek"]={
|
||||
area:"tl",
|
||||
width:state.active?24:0,
|
||||
resetState: initState,
|
||||
getState: function() {
|
||||
return state;
|
||||
},
|
||||
start:start,
|
||||
stop:stop,
|
||||
draw:function() {
|
||||
if (state.active){
|
||||
g.reset();
|
||||
g.drawImage(atob("GBiBAAAAAAAAAAAYAAAYAAAYAAA8AAA8AAB+AAB+AADbAADbAAGZgAGZgAMYwAMYwAcY4AYYYA5+cA3/sB/D+B4AeBAACAAAAAAAAA=="), this.x, this.y);
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
Loading…
Reference in New Issue