From f22c5c8e7feb21ff390d4a176a42f41d03bf468a Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Wed, 21 Sep 2022 21:33:14 +0200 Subject: [PATCH] gpstrek - New app --- apps/gpstrek/ChangeLog | 1 + apps/gpstrek/README.md | 43 ++ apps/gpstrek/app-icon.js | 1 + apps/gpstrek/app.js | 828 ++++++++++++++++++++++++++++++++++++ apps/gpstrek/createRoute.sh | 14 + apps/gpstrek/icon.png | Bin 0 -> 851 bytes apps/gpstrek/metadata.json | 17 + apps/gpstrek/screen1.png | Bin 0 -> 3876 bytes apps/gpstrek/screen2.png | Bin 0 -> 3652 bytes apps/gpstrek/screen3.png | Bin 0 -> 4142 bytes apps/gpstrek/screen4.png | Bin 0 -> 3108 bytes apps/gpstrek/widget.js | 129 ++++++ 12 files changed, 1033 insertions(+) create mode 100644 apps/gpstrek/ChangeLog create mode 100644 apps/gpstrek/README.md create mode 100644 apps/gpstrek/app-icon.js create mode 100644 apps/gpstrek/app.js create mode 100755 apps/gpstrek/createRoute.sh create mode 100644 apps/gpstrek/icon.png create mode 100644 apps/gpstrek/metadata.json create mode 100644 apps/gpstrek/screen1.png create mode 100644 apps/gpstrek/screen2.png create mode 100644 apps/gpstrek/screen3.png create mode 100644 apps/gpstrek/screen4.png create mode 100644 apps/gpstrek/widget.js diff --git a/apps/gpstrek/ChangeLog b/apps/gpstrek/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/gpstrek/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/gpstrek/README.md b/apps/gpstrek/README.md new file mode 100644 index 000000000..eecf4d087 --- /dev/null +++ b/apps/gpstrek/README.md @@ -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. diff --git a/apps/gpstrek/app-icon.js b/apps/gpstrek/app-icon.js new file mode 100644 index 000000000..6b2924353 --- /dev/null +++ b/apps/gpstrek/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIjggOAApMD4AFJg4FF8AFJh/wApMf/AFJn/8ApN//wFDvfeAof774FD+fPLwYFBMAUB8fHAoUDAoJaCgfD4YFIg+D4JgCAosPAoJgCh6DBAoUfAoJgCjwFBvAFBnwFBvgFBngFBngFBvh3BnwFBvH//8eMgQFBMwX//k//5eB//wh//wAFBAQcDRoU/4EDJQfAbYbfFACYA=")) diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js new file mode 100644 index 000000000..091c407fb --- /dev/null +++ b/apps/gpstrek/app.js @@ -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(); diff --git a/apps/gpstrek/createRoute.sh b/apps/gpstrek/createRoute.sh new file mode 100755 index 000000000..729e6af00 --- /dev/null +++ b/apps/gpstrek/createRoute.sh @@ -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" diff --git a/apps/gpstrek/icon.png b/apps/gpstrek/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ff2b99dbd9678c84b8272877a7714593b02915 GIT binary patch literal 851 zcmV-Z1FZasP)M zcbKX-?|pxFQQb8IMHN+4oSJLJKdXztg`^?EY8vnh=m%yb%o0i`fgvkSB+N2mRRZ4_ zHqZx5j-%12IEn&Iz&P9I3ZOBjUXiN;_{8wl&&3ywMP0G^8RbA^JHnuucim0$t9ospauXgi`>U~k-Df}G#CVG47t_lTjY>6R&DrB4f-p2eMIa4 z+!n=ei1~7070`;l9PE)r9ey^Uo+ozQ6uGyef?t4sb(UFkb-*dye~I&pUeq#Nj@lrW zxa8DDVW346%>es=t^knzc;As+bXhesV!S0^_*uX}000@C7SI%6BI@Pseuz2)!0dbs zoE<^49-tZT+ddyS40Mm6ZgW`pD%6wK?+Ud8>rt;6L6t(S{Z{ltk&K_1cP~#J^(dNK zBIz)Ng@P=xBVh`6h$7vnzlv(}MmDR~>LHg{N0?%1;1SS<+T%;3VGN{Ft7n_V5XGG? ds;K``e*l*-;38J^?sNbE002ovPDHLkV1f}vb_W0e literal 0 HcmV?d00001 diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json new file mode 100644 index 000000000..5168c870e --- /dev/null +++ b/apps/gpstrek/metadata.json @@ -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} + ] +} diff --git a/apps/gpstrek/screen1.png b/apps/gpstrek/screen1.png new file mode 100644 index 0000000000000000000000000000000000000000..3cfd7d31b3bd18868a621b80aef5a72345936303 GIT binary patch literal 3876 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 literal 0 HcmV?d00001 diff --git a/apps/gpstrek/screen3.png b/apps/gpstrek/screen3.png new file mode 100644 index 0000000000000000000000000000000000000000..a0c7fd8c312d3518a92713c187ec3b323af9386a GIT binary patch literal 4142 zcma)9`8U+x|9{O`8zgIuwNRF^k2Sl7v1Kn?S+b2#3Xy3TOW6i*@2tsEgOcowEt(?x zT1Je0%OFe45R%V)zn_2LbI-Z=aqhk6JbrjSALnr&H__S>$pI07006*YYJ#w(WA*<{ zR%ZHI)wt|R2T-sr(h#T|5?Z5g7=sN>?OEyL39DBE0B}y2A`I-qT{rVLB4%XR5qSqu z6vAwxi(#Bk2*~q(zT0YfbHb=0D=tho^7Lfe;lu$lZ6BQm{#{xCt8;_MGW}j!S|a%| zorF^JUsP?_Z1*l6E8P0Kt9-8xnEg`s{AW0rSZH9yPhlaVh*{9FFek2X?h>=QR@7F_ zQUZ~s$ywqrvtrD_STDnaIl`M1+SbRYG?5@59NDw`%nt1P$$)MY;ttn5*DVooOC`uW z)Nx)7U5NQ}BnEOQG5hoPi}0Pt6Mi5OgAa`JHeOXy7E0VQA?L9rtS&(Oty5fI7|1cH z+NJnh1r~oA#VmtPU9e11<2h@tw8hSG3RmJd&X4VfIps?OE{ehu4d1$;z$>h)h80(6GToa3(H~*h)T+>3+KjM3~??#;f zRSmtN#FbUP<4>sjgOij^!`im#wJt5bqabB|1S(9U3_)md%lXO%HIOup*a zhPTNN!_BFilDLD^ebnMw*pn5}5JVY{aQ=`G;|2WbGm`(a(%Z_Q{nhH*7+(Dauk$kX zUYFw>YSW)bKEtpmcR-c}>g|~RD1~?vtFyEJSTXNW(Km<}+eV%W_E<%qZKWUu$z08z zU{UXZ@9TSLVXZ$>bmmgv;>ZsiiMWxcNZc&C+>_HOI=Bwq*-tl`a=$2v%9B((_3V3` z?|q$g!A0dGzA=bgI?|Wyz_Wf3og01Y%r@(tkh{e3e)-kVEI#)$mQ-70jBQ^Ea>csO zy{wzE+%nK1>jB4awi0f)oKl~F{#4d2Tw0RHiZ1qq2|r_-592HEYAXaY?L_KIC$mF% ziOe4kM<~WiIiLAMjNUgck^+$0K#FRyTH}61FvbSQQ0wkYoAl z)nPtXdAYkz-pV0K)6wl6IV7v3f)7;ckVku*#=3Qw{nKrR;|L2a8>|f#^Eqch{Q*Ch z1WTIGWlU;_QGH`fXU4F6aLhDA{8z2y?Rxi29TA%N;FbbYky0zLm2J{hG};{v+tru* zS82#^1HMw%Ut}U1EyT1d@2SQRa2w*H&-u#cgyhG@Fhv>?!f5+HrJnWsmXH=4`uCU6 ziF_PO6Cstc`tlt-g$EOf#4glABJoS6o~3FJ0`DbZ@9ohIi>dlPJYk>5Ij*5nYOcp%S8MG{4e-(?Q$G{bS^Ku+_yGYi^EB8W6x)+ zL^87hS@ZoIV$ao0N1idw9HC<&`Wtg z)h63$aCixsd)xUJDU{y!2dZDaqsPI}O8K)htExists@A@}` zuVF{C&-BY(r|k48dGBuq_PZz;M6{eKPtgOi+9fp5ZiktSI(M!alxsXIjW>eRsDzb@ zY^6wl!JDEO21h{(&qYF#C*Mbf!m>Awa`)RP6K%8#U4cj*NsMUdh0s;00KV>xIJ_&T zCBo{y%WOpY1LTG?MgsiA*F+Umv53e|_hzdV)DfLrsAx=|Iv9lh4ajOWa5rUw~4Vb{5_wfZ9(Y3&nMqs zOZJ`Em~|CP*tYcO$Ey{-sS*qHTl=SnhoRc1srj0i9_A{Mm67AOEEbL-S4}hRxEWb; z64zjEAQi=$)$r6nULDQ!w`Q5EF)+3q+($3|h8C!Rl8oTu!NWR<`ZdTdn8VtOaDOJr zytQ*4YO*8C`Ys#(Oo@#z$UHg^Lb zHwU*4($eYIOVfPqr36B&9j3oHe+91kbZ1uOTB^~oFNa#|j1&Dvt1hyBGei{Ep18r$ za3Qtt_3C8oOVaH72Z*VBCq42bf`Y|I&H({EnfQlQ!dVi@#P=duYH78jk9^0!lYI(J zBf|AgWv~??m`vY?Hdf#0=ihxV(Vj#+qjIa)+tT}CE$6IE8x zBYQXWosSH}T*v~#ngyr#a>s01-_y`@HSLI%55w1^#PMO=d!>{uu8DPb{$+aoNKw4R z_&ROu>&Y%d#nxNA|4J6KJ4f^|tNid{Qc@adJLWZf_(C;hM*uEp;(zQs>l0{hE+{K~ zjk7~40kv<`Rq|_Lasv5nXu(a|*&Ejl;tHQ=;gTGVS-l&MJi%idWWn%fig4xz_=_*& zCY_%t(&uj{J~J0N+<1xSxLtZ1C;wAAH$+re|1BJT$O)Jsa#GZO!#tW~F?Nm+2z0&b6%6(JiG| z3OnRJY$@xzG@GrS6};LZzy4gzv?Sm4xHSCNb-^0OwN+W zv!mau2cXe%%_9xF4%;?NXWi1&oYK+ruB{Wv9=3h!?I~RsU~+7jw%PECvpmwWn<-r* zWFqyD%n{aqJ4@F>YgBn$so{n#k$v41$++Tp^TZm9-9@A2=d6@#Ke5-|+=&aV+i;0k4*O{gT$L*L==b%lGQ3aeI6c`0siTZ zf;sAv1F1tjngt(1>LH5d{7L%nOGT`aq5l~7T(K`CX}r+=K)r9rDyRPSWkTHU*&c>4 zxA+-*+%8KB|1}wo#r3aw*}O~06pni^~ZrJ;NQ+C z4hOD4)y)q_=5)ncX4VO~R1lW{S2=fDSOqUTS%$UUDComZLo2 z<)L}O=xB`YSO!YoInf`*|ABfEQ2iO+Ag}$aQlFF8)^iQLjNCFr96{rjTFOF4_W~8I z&2)E*A6mFsKnIa!ZkORfwchoE*Gl;Q`uQ?Fo9TQ-SxX_JinD3~fGG4Tcd< zlCG}*;s@$%(xZp#(*Nm%B?O*3Ph@Cw{zn;<)#54EG+421M}}0Fs@`SO@jWpAA(Ue} zaco=xAjKuchmPHQ)5cy4@<{m<8WyEDr;Uoid7xjk8#QA5wr}@2hIsbfW2nw}U+2bi zz?BCL=gr~6AOfdr+Ktppe~tq#wuv^71yjC}uTKlMS%>7+l&lK~V)?#$zAAK?bu%cU zLpln)6b-d&aNh91GdIO6Mr;JWPIUB9H!?)CN<;UU{{TXq-gW>0 literal 0 HcmV?d00001 diff --git a/apps/gpstrek/screen4.png b/apps/gpstrek/screen4.png new file mode 100644 index 0000000000000000000000000000000000000000..7b681207729925f13e7d849a9184ae6245feb297 GIT binary patch literal 3108 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 yL*S(d%oiCWFhHHBc>n@KU { +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); + } + } +}; +})();