diff --git a/apps/rungps/ChangeLog b/apps/rungps/ChangeLog
new file mode 100644
index 0000000000..a54add3cf8
--- /dev/null
+++ b/apps/rungps/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Download capebillity and nan heart rate error fixed
diff --git a/apps/rungps/Home.css b/apps/rungps/Home.css
new file mode 100644
index 0000000000..9e94fbff4b
--- /dev/null
+++ b/apps/rungps/Home.css
@@ -0,0 +1,20 @@
+.data {
+ border-bottom: #838383 solid 2px;
+ border-top: #838383 solid 2px;
+ width: 50%;
+ position: absolute;
+ top: 50%;
+}
+
+#map {
+ height: 50%;
+ width: 50%;
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+
+#inputDiv {
+ position: absolute;
+ top: calc(50% + 100px);
+}
diff --git a/apps/rungps/README.md b/apps/rungps/README.md
new file mode 100644
index 0000000000..1954421037
--- /dev/null
+++ b/apps/rungps/README.md
@@ -0,0 +1,42 @@
+# Running
+
+This app is designed to be simple and intuitive.
+I’ve tried to make everything as clear as possible.
+
+## How to Use
+
+When you open the app, you have two options:
+
+- **Run** – start a new running session.
+- **Results** – view your previous runs.
+
+---
+
+## Run
+
+When you tap **Run**, you’ll see four squares with a red one in the middle.
+
+- To start running, press **Button 1** on the side of your **Bangle.js**.
+ The middle square will turn **green**, indicating that recording has started.
+
+- To stop, press the same button again.
+ The screen will freeze, showing that the session has ended.
+
+To view your results, reopen the app and choose **Results**.
+
+---
+
+## Results
+
+In the **Results** screen, you’ll see a list of your runs sorted by date.
+Tap any date to view detailed data for that session.
+
+You can also view or download your results directly from the **App Loader**.
+
+---
+
+## Notes
+
+- Make sure your **GPS** is active before starting a run. It is ready when the distance and speed starts changing.
+- Recorded data includes time, distance, heart rate, ...
+- Results are saved on the watch and can be exported for analysis.
diff --git a/apps/rungps/app-icon.js b/apps/rungps/app-icon.js
new file mode 100644
index 0000000000..e19e2ba696
--- /dev/null
+++ b/apps/rungps/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UA///IfsVqALJqtUBREBqEVBZNAgoIFgofBBZFVqtQBY4rBgNVBY8VoAlBoBHGAoRGCqAYDA4I7CB4VVDwIfCioGBE4VRBYUUgNUDQQ/BoILDDAJFBB4MFBYcFqoCBGgJjBCQI7DDQINBDAR4FIoIIBqAQBPApvDBYopCOwTMFFIR3BooLFDoJUCOwYLGag5SCcBDqBM4bXGFoI4FLwVRBILJFBYVAO4YAFYASpCBYwwBEgIwGZgNQQI43BEAJVBBYzgBDQZGGCoIaCIwwtCKYzSCCoLIFXIIJBBwJ3FqkUBIL5BHQsVDAILBU40FNAUFqqZGQAUBFwoKBAALHHBZYAj"))
diff --git a/apps/rungps/app.js b/apps/rungps/app.js
new file mode 100644
index 0000000000..5277698e1a
--- /dev/null
+++ b/apps/rungps/app.js
@@ -0,0 +1,223 @@
+var storage = require("Storage");
+var width = g.getWidth();
+var height = g.getHeight();
+
+var gps = {};
+var heart_rate = {bpm: 0, confidence: 0};
+var running = false;
+var al_fix = false;
+var al_rate = false;
+var typen = false;
+var vorig = null;
+var total_distance = 0;
+var start_time = 0|getTime();
+var watch;
+var interval;
+
+var mainMenu;
+
+function central(text, x, x2, y) {
+ g.drawString(text, (x2 - g.stringWidth(text))/2+x, y);
+}
+
+function getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2) {
+ var R = 6371;
+ var dLat = deg2rad(lat2-lat1);
+ var dLon = deg2rad(lon2-lon1);
+ var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
+ Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
+ Math.sin(dLon/2) * Math.sin(dLon/2);
+ var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+ var d = R * c;
+ return d;
+}
+
+function deg2rad(deg) {
+ return deg * (Math.PI/180);
+}
+
+function write(f) {
+ var csv = [
+ 0|getTime(),
+ gps.lat,
+ gps.lon,
+ gps.alt,
+ gps.speed,
+ gps.course,
+ heart_rate.bpm
+ ];
+ f.write(csv.join(", ")+"\n");
+}
+
+function getTimediffrence() {
+ var diffrence = 0|getTime() - start_time;
+ return [Math.floor(diffrence / 60), diffrence % 60];
+}
+
+function scherm() {
+ var speed = 0;
+ var diffrence = getTimediffrence();
+ if (gps.fix) {
+ if (gps.speed != 0) {
+ speed = 60/gps.speed;
+ }
+ }
+ g.setColor(0, 0, 0);
+ g.clear();
+ g.setFont("Vector", 15);
+ g.drawLine(width/2, 0, width/2, height);
+ g.drawLine(0, height/2, width, height/2);
+ central("Speed", 0, width/2, 5);
+ central("Distance", width/2, width/2, 5);
+ central("Heart rate", 0, width/2, height/2+5);
+ central("Time", width/2, width/2, height/2+5);
+ g.setFont("Vector", 30);
+ if (gps.fix) {
+ central(Math.floor(speed)+":"+Math.floor((speed * 60) % 60), 0, width/2, 20);
+ if (vorig) {
+ total_distance += getDistanceFromLatLonInKm(vorig.lat, vorig.lon, gps.lat, gps.lon);
+ central(Math.floor(total_distance*100)/100, width/2, width/2, 20);
+ }
+ }
+ central(heart_rate.bpm, 0, width/2, height/2+20);
+ central(diffrence[0]+":"+diffrence[1], width/2, width/2, height/2+20);
+ if (running) {
+ g.setColor(0, 1, 0);
+ } else {
+ g.setColor(1, 0, 0);
+ }
+ g.fillRect(width/2-8, height/2-8, width/2+8, height/2+8);
+}
+
+function maakklaar(f) {
+ if (!typen) {
+ var fix = Bangle.getGPSFix();
+ gps = fix;
+ scherm();
+ if (fix.fix) {
+ if (!al_fix) {
+ Bangle.buzz();
+ console.log(fix.satellites);
+ al_fix = true;
+ }
+ if (!al_rate) {
+ al_rate = true;
+ }
+ if (running) {
+ write(f);
+ }
+ vorig = gps;
+ }
+ }
+}
+
+function run() {
+ var f = storage.open("rungps."+require("locale").date(new Date())+".csv","a");
+ watch = setWatch(() => {running = !running;
+ if (running === false) {
+ f.write("]");
+ stopRun();
+ } else {
+ start_time = 0|getTime();
+ }
+ }, BTN1, {repeat: true});
+ interval = setInterval(maakklaar, 2000, [f]);
+
+ Bangle.setGPSPower(1, "rungps");
+ Bangle.setHRMPower(1, "rungps");
+ Bangle.on("HRM", (r) => { heart_rate = Number(r); });
+}
+
+function stopRun() {
+ clearWatch(watch);
+ clearInterval(interval);
+
+ Bangle.setGPSPower(0, "rungps");
+ Bangle.setHRMPower(0, "rungps");
+ Bangle.removeListener("HRM", (r) => { heart_rate = Number(r); });
+}
+
+function parseFile(file) {
+ var totalHeart_rate = 0;
+ var distance = 0;
+ var points = 0;
+ var endTime = 0;
+
+ var l = file.readLine();
+ var all = l.split(", ");
+ const startTime = all[0];
+ var lat = all[1];
+ var lon = all[2];
+ totalHeart_rate += all[6]/10000;
+ points += 1;
+
+ var vorig = {lat: lat, lon: lon};
+
+ l = file.readLine();
+ while (l[0] !== "]") {
+ all = l.split(", ");
+ var time = all[0];
+ lat = all[1];
+ lon = all[2];
+
+ distance += getDistanceFromLatLonInKm(vorig.lat, vorig.lon, lat, lon);
+ totalHeart_rate += all[6]/10000;
+ points += 1;
+ vorig = {lat: lat, lon: lon};
+ l = file.readLine();
+ if (l[0] === "]") {
+ endTime = time;
+ }
+ }
+ var timeDifference = endTime - startTime;
+ var speed = Math.floor(timeDifference/distance);
+ distance = Math.floor(distance*100)/100;
+ return [Math.floor(totalHeart_rate/points*10000), distance.toString()+"km", convertSecondsToTime(timeDifference),
+ Math.floor(speed/60)+":"+Math.floor(speed % 60)+"min/km"];
+}
+
+function convertSecondsToTime(given_seconds) {
+ var seconds = given_seconds % 60;
+ var minutes = Math.floor(given_seconds/60) % 60;
+ var hours = Math.floor(given_seconds/60/60);
+ return hours+":"+minutes+":"+seconds;
+}
+
+function showData(file) {
+ var title = file.replace("rungps.","").replace(".csv","");
+ E.showMessage(/*LANG*/"Loading...", title);
+ var f = storage.open(file, "r");
+ var all = parseFile(f);
+ var heart_rate = all[0];
+ var distance = all[1];
+ var timeDifference = all[2];
+ var speed = all[3];
+ E.showMenu({
+ "": {title: file.replace("rungps.","").replace(".csv","")},
+ "Speed": {value: speed},
+ "Distance": {value: distance},
+ "Time": {value: timeDifference},
+ "Heart rate": {value: heart_rate},
+ "< Back": () => E.showMenu(mainMenu)
+ });
+}
+
+function results() {
+ var allFiles = storage.list(/rungps\..+\.csv$/, {sf: true});
+ E.showScroller({
+ h : 40, c : allFiles.length,
+ draw : (idx, r) => {
+ g.setBgColor((idx&1)?"#666":"#CCC").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1);
+ g.setFont("6x8:2").drawString(allFiles[idx].replace("rungps.","").replace(".csv",""), r.x+10,r.y+4);
+ },
+ select : (idx) => showData(allFiles[idx])
+ });
+}
+
+mainMenu = {
+ "": { title: "-- Running --" }, // options
+ "Run": () => run(),
+ "Results": () => results()
+};
+E.showMenu(mainMenu);
+
diff --git a/apps/rungps/app.png b/apps/rungps/app.png
new file mode 100644
index 0000000000..883245b814
Binary files /dev/null and b/apps/rungps/app.png differ
diff --git a/apps/rungps/calculations.js b/apps/rungps/calculations.js
new file mode 100644
index 0000000000..440ef40291
--- /dev/null
+++ b/apps/rungps/calculations.js
@@ -0,0 +1,57 @@
+/* eslint-disable */
+window.calculations = {
+ "translateData": function (text) {
+ var points = []
+ for (var line of text.split("\n")) {
+ var all = line.split(", ");
+ if (all.length === 7) {
+ let [time_seconds, lat, lon, alt, speed, course, heart_rate] = all;
+ points.push({lat: parseFloat(lat), long: parseFloat(lon), time: time_seconds, heart_rate: heart_rate, speed: speed, alt: alt});
+ }
+ }
+ return points;
+ },
+ "deg2rad": function (deg) {
+ return deg * (Math.PI/180);
+ },
+ "getDistanceFromLatLonInKm": function (lat1, lon1, lat2, lon2) {
+ const R = 6371; // Radius of the earth in km
+ var dLat = window.calculations.deg2rad(lat2-lat1); // deg2rad below
+ var dLon = window.calculations.deg2rad(lon2-lon1);
+ var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
+ Math.cos(window.calculations.deg2rad(lat1)) * Math.cos(window.calculations.deg2rad(lat2)) *
+ Math.sin(dLon/2) * Math.sin(dLon/2);
+ var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+ return R * c;
+ },
+ "getAverageSpeedAndDistance": function (points) {
+ var timeDifference = points[points.length-1].time - points[0].time;
+ var distance = 0;
+ var vorig = points[0];
+ for (let i = 1; i < points.length; i++) {
+ distance += window.calculations.getDistanceFromLatLonInKm(vorig.lat, vorig.long, points[i].lat, points[i].long);
+ vorig = points[i]
+ }
+ distance = Math.floor(distance*100)/100;
+ var speed = Math.floor(timeDifference/distance)
+ return [Math.floor(speed/60)+":"+Math.floor(speed % 60)+"min/km", distance.toString()+"km"];
+ },
+ "convertSecondstoTime": function (given_seconds) {
+ let dateObj = new Date(given_seconds * 1000);
+ let hours = dateObj.getUTCHours();
+ let minutes = dateObj.getUTCMinutes();
+ let seconds = dateObj.getSeconds();
+
+ return hours.toString().padStart(2, '0')
+ + ':' + minutes.toString().padStart(2, '0')
+ + ':' + seconds.toString().padStart(2, '0');
+ },
+ "getIndexByTime": function (time, points) {
+ for (let i = 0; i < points.length; i++) {
+ if (window.calculations.convertSecondstoTime(points[i].time) === time) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/apps/rungps/gpx_maker.js b/apps/rungps/gpx_maker.js
new file mode 100644
index 0000000000..5475e28162
--- /dev/null
+++ b/apps/rungps/gpx_maker.js
@@ -0,0 +1,252 @@
+/* eslint-disable */
+const createXmlString = lines => {
+ // Normalize input into an array of segments, each a list of point objects
+ // Supported inputs:
+ // 1) [[lon,lat,ele], ...] -> single segment
+ // 2) [ [ [lon,lat,ele], ... ], [ [lon,lat,ele] ] ] -> multiple segments
+ // 3) [{lat, long, time, heart_rate, speed, alt}, ...] -> single segment (new format)
+ // 4) [ [ {..}, {..} ], [ {..} ] ] -> multiple segments (new format)
+ const toPointObj = (p) => {
+ if (p && typeof p === 'object' && !Array.isArray(p)) {
+ // already object with possible fields
+ return {
+ lat: typeof p.lat === 'string' ? parseFloat(p.lat) : p.lat,
+ long: typeof p.long === 'string' ? parseFloat(p.long) : p.long,
+ alt: p.alt != null ? (typeof p.alt === 'string' ? parseFloat(p.alt) : p.alt) : undefined,
+ time: p.time != null ? (typeof p.time === 'string' ? parseFloat(p.time) : p.time) : undefined,
+ heart_rate: p.heart_rate != null ? (typeof p.heart_rate === 'string' ? parseFloat(p.heart_rate) : p.heart_rate) : undefined,
+ speed: p.speed != null ? (typeof p.speed === 'string' ? parseFloat(p.speed) : p.speed) : undefined,
+ };
+ }
+ // legacy array [lon, lat, ele]
+ if (Array.isArray(p)) {
+ return { lat: parseFloat(p[1]), long: parseFloat(p[0]), alt: p[2] != null ? parseFloat(p[2]) : undefined };
+ }
+ return undefined;
+ };
+
+ let segments = [];
+ if (!Array.isArray(lines)) {
+ segments = [];
+ } else if (lines.length && Array.isArray(lines[0])) {
+ // Could be [[lon,lat,ele], ...] or [ [ [lon,lat,ele], ... ], ...]
+ if (lines[0].length && Array.isArray(lines[0][0])) {
+ // multiple segments of arrays
+ segments = lines.map(seg => seg.map(toPointObj));
+ } else if (lines[0].length && (typeof lines[0][0] === 'number' || typeof lines[0][0] === 'string')) {
+ // single segment of arrays
+ segments = [lines.map(toPointObj)];
+ } else if (lines[0] && typeof lines[0] === 'object' && !Array.isArray(lines[0])) {
+ // single segment of objects nested in an array wrapper
+ segments = [lines.map(toPointObj)];
+ } else {
+ segments = [];
+ }
+ } else if (lines.length && typeof lines[0] === 'object') {
+ // single segment of objects
+ segments = [lines.map(toPointObj)];
+ } else {
+ segments = [];
+ }
+
+ // Compute base time if any time values exist (seconds, relative). We'll base on current time.
+ let firstTime = undefined;
+ for (const seg of segments) {
+ for (const p of seg) {
+ if (p && typeof p.time === 'number' && !isNaN(p.time)) {
+ firstTime = (firstTime === undefined) ? p.time : Math.min(firstTime, p.time);
+ }
+ }
+ }
+ const baseDate = new Date();
+
+ let result = '
0km
+0:0min/km
+