/**
* Six Minute Walk Test App: Algorithm for the outdoor test.
* This code is written in JavaScript.
* The code has been slightly adapted from the original to make it more readable.
* Authors: Dario Salvi
*/
// Maximum allowable speed in m/s
var MAX_SPEED = 2
// Minimum accuracy to be used to pass the signal check routine
var CHECKSIGNAL_MINACCURACY = 15
// Period, in seconds, when to select a sample
var SELECTION_PERIOD = 5
// Buffer of all position samples
var positions = []
// Holder of the positions that were selected by the algorithm
var selectedPositions = []
// Total walked distance
var distance = 0
// Tells if the actual test has started
var started = false
/**
* Tells the algorithm that the test has officially started
*/
function startTest() {
distance = 0
started = true
selectedPositions = []
// select the starting position
var selected = selectPosition(positions[0].timestamp, SELECTION_PERIOD / 4)
if (!selected) { // if there is no candidate for selection just use the last one
selected = positions[0]
}
selectedPositions.unshift(selected)
}
/**
* Tells the algorithm that the test has officially ended
*/
function stopTest() {
started = false
// if there were no detected steps, then just give zero
if (positions[0].steps !== undefined && positions[0].steps === 0) {
distance = 0
return
}
// select the last distance sample
var selected = selectPosition(positions[0].timestamp, 10) // for the last sample, let's focus on the last 10 seconds
if (!selected) { // if there is no candidate for selection, then just use the last one
selected = positions[0]
}
if (selected.timestamp !== selectedPositions[0].timestamp) { // it may happen that this position was already selected
distance += crowDist(selectedPositions[0], selected)
selectedPositions.unshift(selected)
}
}
/**
* A position is available and has to be computed.
* This must be called each time a new position sample is available
* @param position: the position object as for the Web location API
* example: { timestamp: ttt, coords: {longitude: xx, latitude: yy, accuracy: zz,a ltitude: bbb}, steps: ss}
* If a step counter is availble, steps can be added to this object
*/
function addPosition(position) {
positions.unshift(position)
if (started) {
// select the sample if enough time has passed
if ((position.timestamp - selectedPositions[0].timestamp) >= (SELECTION_PERIOD * 1000)) {
// select the best one within a reasonable time window
var selected = selectPosition(position.timestamp, SELECTION_PERIOD / 4)
if (selected) {
distance += crowDist(selectedPositions[0], selected)
selectedPositions.unshift(selected)
}
}
}
}
/**
* Tells if the signal is of enough quality
*/
function isSignalOK() {
// we define "enough quality" when there is altitude (means that the GPS is on)
// and the accuracy is less than CHECKSIGNAL_MINACCURACY
var lastP = positions[0]
return lastP && (lastP.coords.altitude) && (lastP.coords.accuracy <= CHECKSIGNAL_MINACCURACY)
}
/**
* Computes the latest available distance
* if the test is not stopped it will give the best estimation
*/
function getDistance() {
if (started) {
// the distance to be shown when the test is running, does not accumulate
var showdistance = 0
// if there are no new steps, just return the distance
if ((positions.length > 1) && (positions[0].steps || (positions[0].steps === 0)) && ((positions[0].steps - positions[1].steps) === 0)) {
return distance
}
// otherwise, return the total distance plus the difference between the latest sample and the latest selected position
var d = crowDist(selectedPositions[0], positions[0])
showdistance = distance + d
return showdistance
} else {
// when not running, give the total one
return distance
}
}
// gives the distance between two points in direct line (crow flight distance)
// latitude and longitude are in decimal degrees
// returns the distance in meters
function crowDist(point1, point2) {
var lat1 = point1.coords.latitude
var lat2 = point2.coords.latitude
var lon1 = point1.coords.longitude
var lon2 = point2.coords.longitude
var R = 6371 // km
var dLat = toRad(lat2 - lat1)
var dLon = toRad(lon2 - lon1)
lat1 = toRad(lat1)
lat2 = toRad(lat2)
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2)
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
var d = R * c
return d * 1000
}
// Converts numeric degrees to radians
function toRad(degr) {
return degr * Math.PI / 180
}
// selects a position sample with the best accuracy within a time window
// time: current time, as Unixtimestamp
// secs: how big can the time window be
function selectPosition(time, secs) {
// if there are no new steps, don't compute distance
if ((positions.length > 1) && positions[0].steps && ((positions[0].steps - positions[1].steps) === 0))
return null
// use the best sample within secs
// "best" is the one with highest accuracy and that does not suppose extreme speeds (>MAX_SPEED hour)
var bestAccuracy = 10000
// index of the sample with best accuracy
var bestAccuracyI = -1
for (var i = 0; i < positions.length; i++) {
var pos = positions[i]
if (time - pos.timestamp > (secs * 1000)) {
// we don't have to go further
if (bestAccuracyI >= 0) {
// there's a candidate
return positions[bestAccuracyI] // returns the one with the best accuracy
} else { // there are no suitable options in this time window :(
return null;
}
}
if (pos.coords.accuracy < 5) {
return pos // 5m error, that's enough accuracy! no need to go further
}
// compute speed since last selected point
var speed = 0;
if (selectedPositions.length > 0) { // (only possible when there is at least one selected)
speed = crowDist(pos, selectedPositions[0]) / ((time - selectedPositions[0].timestamp) / 1000) // m/s
}
if (speed < MAX_SPEED) {
if (pos.coords.accuracy < bestAccuracy) {
bestAccuracy = positions[i].coords.accuracy
bestAccuracyI = i
}
} else {
// just ignore this point
// console.log('what a jump! speed: '+speed, pos);
}
}
return positions[bestAccuracyI]
}