Last active
March 19, 2018 18:56
-
-
Save SethHamilton/b6e6f1c0fa00da2805fcd0b813db5adc to your computer and use it in GitHub Desktop.
Nice fit point reducer for mapbox
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Calculate the bearing between two positions as a value from 0-360 | |
* | |
* @param lat1 - The latitude of the first position | |
* @param lng1 - The longitude of the first position | |
* @param lat2 - The latitude of the second position | |
* @param lng2 - The longitude of the second position | |
* | |
* @return int - The bearing between 0 and 360 | |
*/ | |
bearing : function (lat1,lng1,lat2,lng2) { | |
var dLon = this._toRad(lng2-lng1); | |
var y = Math.sin(dLon) * Math.cos(this._toRad(lat2)); | |
var x = Math.cos(this._toRad(lat1))*Math.sin(this._toRad(lat2)) - Math.sin(this._toRad(lat1))*Math.cos(this._toRad(lat2))*Math.cos(dLon); | |
var brng = this._toDeg(Math.atan2(y, x)); | |
return ((brng + 360) % 360); | |
}, | |
/** | |
* Since not all browsers implement this we have our own utility that will | |
* convert from degrees into radians | |
* | |
* @param deg - The degrees to be converted into radians | |
* @return radians | |
*/ | |
_toRad : function(deg) { | |
return deg * Math.PI / 180; | |
}, | |
/** | |
* Since not all browsers implement this we have our own utility that will | |
* convert from radians into degrees | |
* | |
* @param rad - The radians to be converted into degrees | |
* @return degrees | |
*/ | |
_toDeg : function(rad) { | |
return rad * 180 / Math.PI; | |
}, | |
haversineDistanceKm : function(lat1, lon1, lat2, lon2) { | |
var earthRadiusKm = 6371; | |
var dLat = this._toRad(lat2-lat1); | |
var dLon = this._toRad(lon2-lon1); | |
lat1 = this._toRad(lat1); | |
lat2 = this._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)); | |
return earthRadiusKm * c; | |
}, | |
destVincenty: function(lat1, lon1, brng, dist) { | |
var a = 6378137, | |
b = 6356752.3142, | |
f = 1 / 298.257223563, // WGS-84 ellipsiod | |
s = dist, | |
alpha1 = this._toRad(brng), | |
sinAlpha1 = Math.sin(alpha1), | |
cosAlpha1 = Math.cos(alpha1), | |
tanU1 = (1 - f) * Math.tan(this._toRad(lat1)), | |
cosU1 = 1 / Math.sqrt((1 + tanU1 * tanU1)), sinU1 = tanU1 * cosU1, | |
sigma1 = Math.atan2(tanU1, cosAlpha1), | |
sinAlpha = cosU1 * sinAlpha1, | |
cosSqAlpha = 1 - sinAlpha * sinAlpha, | |
uSq = cosSqAlpha * (a * a - b * b) / (b * b), | |
A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))), | |
B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))), | |
sigma = s / (b * A), | |
sigmaP = 2 * Math.PI; | |
while (Math.abs(sigma - sigmaP) > 1e-12) { | |
var cos2SigmaM = Math.cos(2 * sigma1 + sigma), | |
sinSigma = Math.sin(sigma), | |
cosSigma = Math.cos(sigma), | |
deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM))); | |
sigmaP = sigma; | |
sigma = s / (b * A) + deltaSigma; | |
}; | |
var tmp = sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1, | |
lat2 = Math.atan2(sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1, (1 - f) * Math.sqrt(sinAlpha * sinAlpha + tmp * tmp)), | |
lambda = Math.atan2(sinSigma * sinAlpha1, cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1), | |
C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha)), | |
L = lambda - (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM))), | |
revAz = Math.atan2(sinAlpha, -tmp); // final bearing | |
return [lon1 + this._toDeg(L), this._toDeg(lat2)]; | |
}, | |
distVincenty: function(lat1, lon1, lat2, lon2) { | |
var a = 6378137, | |
b = 6356752.3142, | |
f = 1 / 298.257223563, // WGS-84 ellipsoid params | |
L = toRad(lon2-lon1), | |
U1 = Math.atan((1 - f) * Math.tan(toRad(lat1))), | |
U2 = Math.atan((1 - f) * Math.tan(toRad(lat2))), | |
sinU1 = Math.sin(U1), | |
cosU1 = Math.cos(U1), | |
sinU2 = Math.sin(U2), | |
cosU2 = Math.cos(U2), | |
lambda = L, | |
lambdaP, | |
iterLimit = 100; | |
do { | |
var sinLambda = Math.sin(lambda), | |
cosLambda = Math.cos(lambda), | |
sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)); | |
if (0 === sinSigma) { | |
return 0; // co-incident points | |
}; | |
var cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda, | |
sigma = Math.atan2(sinSigma, cosSigma), | |
sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma, | |
cosSqAlpha = 1 - sinAlpha * sinAlpha, | |
cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha, | |
C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha)); | |
if (isNaN(cos2SigmaM)) { | |
cos2SigmaM = 0; // equatorial line: cosSqAlpha = 0 (§6) | |
}; | |
lambdaP = lambda; | |
lambda = L + (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM))); | |
} while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0); | |
if (!iterLimit) { | |
return NaN; // formula failed to converge | |
}; | |
var uSq = cosSqAlpha * (a * a - b * b) / (b * b), | |
A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))), | |
B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))), | |
deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM))), | |
s = b * A * (sigma - deltaSigma); | |
return s.toFixed(3); // round to 1mm precision | |
} | |
}; | |
function __reducer(coordinates) { | |
var simplified = [] | |
var coordinatesIndex = 0 | |
var capturedSequence = [] | |
var startBearing = 0 | |
while (coordinatesIndex < coordinates.length) | |
{ | |
if (capturedSequence.length == 2) | |
{ | |
startBearing = geo.bearing( | |
capturedSequence[0][1], capturedSequence[0][0], | |
capturedSequence[1][1], capturedSequence[1][0] | |
) | |
} | |
else if (capturedSequence.length > 2) | |
{ | |
var idx = capturedSequence.length - 1 | |
const dist = distVincenty( | |
capturedSequence[0][1], capturedSequence[0][0], | |
capturedSequence[idx][1], capturedSequence[idx][0] | |
) | |
const target = destVincenty( | |
capturedSequence[0][1], capturedSequence[0][0], | |
startBearing, | |
dist | |
) | |
const diffLat = Math.abs(target[1] - capturedSequence[idx][1]) | |
const diffLng = Math.abs(target[0] - capturedSequence[idx][0]) | |
// +/- 8ish meters depending on where in the world you are | |
if (diffLat >= 0.0002 || diffLng >= 0.0002) { | |
if (!simplified.length) // || (diffBearing != -1 && diffBearing > 11.25)) | |
simplified.push(capturedSequence[0]) | |
simplified.push(capturedSequence[idx-1]) | |
lastBearing = startBearing | |
capturedSequence = [capturedSequence[idx-1], capturedSequence[idx]] | |
continue | |
} | |
} | |
capturedSequence.push([coordinates[coordinatesIndex][0],coordinates[coordinatesIndex][1]]) | |
++coordinatesIndex; | |
} | |
capturedSequence.forEach(item => { | |
simplified.push(item) | |
}) | |
return simplified | |
} | |
function pathReducer(coordinates, precision) { | |
var lastLen = 0 | |
var multiplier = 1 | |
for (var i = 0; i < precision; ++i) { | |
multiplier *= 10 | |
} | |
while (lastLen != coordinates.length) | |
{ | |
lastLen = coordinates.length | |
coordinates = __reducer(coordinates, multiplier) | |
} | |
var dedupedCoordinates = [] | |
coordinates.forEach(entry => { | |
// reduce the number of decimal places by amount in precision | |
entry = [ | |
Math.round(entry[0] * multiplier) / multiplier, | |
Math.round(entry[1] * multiplier) / multiplier | |
] | |
// remove potential dupes after reducing | |
if (!dedupedCoordinates.length || | |
dedupedCoordinates[dedupedCoordinates.length-1][0] != entry[0] || | |
dedupedCoordinates[dedupedCoordinates.length-1][1] != entry[1]) | |
dedupedCoordinates.push(entry) | |
}) | |
return dedupedCoordinates | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
MIT License - please enjoy and improve