Skip to content

Instantly share code, notes, and snippets.

@SethHamilton
Last active March 19, 2018 18:56
Show Gist options
  • Save SethHamilton/b6e6f1c0fa00da2805fcd0b813db5adc to your computer and use it in GitHub Desktop.
Save SethHamilton/b6e6f1c0fa00da2805fcd0b813db5adc to your computer and use it in GitHub Desktop.
Nice fit point reducer for mapbox
/**
* 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
}
@SethHamilton
Copy link
Author

MIT License - please enjoy and improve

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment