@bitblit/ratchet-common
Version:
Common tools for general use
264 lines • 10.8 kB
JavaScript
import { RequireRatchet } from './require-ratchet.js';
import { NumberRatchet } from './number-ratchet.js';
import { ErrorRatchet } from './error-ratchet.js';
import { Logger } from '../logger/logger.js';
export class GeolocationRatchet {
constructor() { }
static distanceBetweenLocations(lat1, lon1, lat2, lon2, unit = 'M') {
const uU = unit ? unit.toUpperCase() : '';
if (['M', 'K', 'N', 'F', 'E'].indexOf(uU) === -1) {
throw new Error('Invalid unit');
}
if (lat1 == lat2 && lon1 == lon2) {
return 0;
}
else {
const radlat1 = (Math.PI * lat1) / 180;
const radlat2 = (Math.PI * lat2) / 180;
const theta = lon1 - lon2;
const radtheta = (Math.PI * theta) / 180;
let dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
if (dist > 1) {
dist = 1;
}
dist = Math.acos(dist);
dist = (dist * 180) / Math.PI;
dist = dist * 60 * 1.1515;
if (uU === 'F') {
dist *= 5280;
}
if (uU === 'K') {
dist *= 1.609344;
}
if (uU === 'E') {
dist *= 1609.344;
}
if (uU === 'N') {
dist *= 0.8684;
}
return dist;
}
}
static distanceBetweenRatchetGeoLocations(loc1, loc2, unit = 'M') {
return GeolocationRatchet.distanceBetweenLocations(loc1.lat, loc1.lng, loc2.lat, loc2.lng, unit);
}
static degreeOfLatLngInMiles(latitudeInDecimalDegress = 0) {
const latInRads = (latitudeInDecimalDegress * Math.PI) / 180;
const cosLat = Math.cos(latInRads);
const rval = NumberRatchet.safeNumber((cosLat * 69.172).toFixed(4));
return rval;
}
static milesInDegLatLng(miles, latitudeInDecimalDegress = 0) {
RequireRatchet.notNullOrUndefined(miles);
RequireRatchet.true(miles >= 0);
const degreeInMiles = GeolocationRatchet.degreeOfLatLngInMiles(latitudeInDecimalDegress);
return miles / degreeInMiles;
}
static centerOfBounds(bounds) {
RequireRatchet.notNullOrUndefined(bounds);
return {
lat: (bounds.extent.lat + bounds.origin.lat) / 2,
lng: (bounds.extent.lng + bounds.origin.lng) / 2,
};
}
static calculateSplits(input, slices, field) {
RequireRatchet.notNullOrUndefined(input);
RequireRatchet.notNullOrUndefined(slices);
RequireRatchet.notNullOrUndefined(field);
input.sort((a, b) => a.origin[field] - b.origin[field]);
const centers = input.map((i) => GeolocationRatchet.centerOfBounds(i));
const vals = centers.map((c) => c[field]);
const splits = [];
for (let i = 1; i < vals.length; i++) {
const size = vals[i] - vals[i - 1];
if (splits.length < slices) {
splits.push({ idx: i, size: size });
splits.sort((a, b) => a.size - b.size);
}
else if (size > splits[0].size) {
splits[0] = { idx: i, size: size };
splits.sort((a, b) => a.size - b.size);
}
else {
Logger.silly('Skipping, size : %d, %j', size, splits);
}
}
Logger.info('Splits at : %j', splits);
splits.sort((a, b) => a.idx - b.idx);
return splits;
}
static clusterGeoBounds(inputVal, latSlices = 2, lngSlices = 5) {
let rval = null;
if (latSlices * lngSlices < 2) {
ErrorRatchet.throwFormattedErr('Cannot set slices to less than 2 : %d x %d', latSlices, lngSlices);
}
if (inputVal) {
rval = [];
const input = Object.assign([], inputVal);
input.sort((a, b) => a.origin.lng - b.origin.lng);
const lngSplits = GeolocationRatchet.calculateSplits(inputVal, lngSlices - 1, 'lng');
lngSplits.sort((a, b) => a.idx - b.idx);
for (let i = 0; i <= lngSplits.length; i++) {
const lngStartIdx = i === 0 ? 0 : lngSplits[i - 1].idx;
const lngEndIdx = i === lngSplits.length ? input.length : lngSplits[i].idx;
const lngBatch = input.slice(lngStartIdx, lngEndIdx);
lngBatch.sort((a, b) => a.origin.lat - b.origin.lat);
const latSplits = GeolocationRatchet.calculateSplits(lngBatch, latSlices - 1, 'lat');
for (let j = 0; j <= latSplits.length; j++) {
const latStartIdx = j == 0 ? 0 : latSplits[j - 1].idx;
const latEndIdx = j === latSplits.length ? lngBatch.length : latSplits[j].idx;
const latBatch = lngBatch.slice(latStartIdx, latEndIdx);
rval.push(GeolocationRatchet.combineBounds(latBatch));
}
}
Logger.info('New bounds : %j', rval);
}
return rval;
}
static canonicalizeBounds(inp, allowCrossover = false) {
RequireRatchet.notNullOrUndefined(inp, 'RatchetLocationBounds');
const minLat = Math.min(inp.extent.lat, inp.origin.lat);
const maxLat = Math.max(inp.extent.lat, inp.origin.lat);
const minLng = Math.min(inp.extent.lng, inp.origin.lng);
const maxLng = Math.max(inp.extent.lng, inp.origin.lng);
const latXover = (minLat < 0 && maxLat > 0) || (minLat > 0 && maxLat < 0);
const lngXover = (minLat < 0 && maxLat > 0) || (minLat > 0 && maxLat < 0);
if (latXover || lngXover) {
if (allowCrossover) {
return inp;
}
else {
throw new Error('Cannot canonicalize, bounds crosses over boundary');
}
}
const rval = {
origin: {
lat: minLat,
lng: minLng,
},
extent: {
lat: maxLat,
lng: maxLng,
},
};
return rval;
}
static combineBounds(inp) {
let rval = null;
if (inp && inp.length > 0) {
rval = {
origin: {
lat: inp.map((i) => i.origin.lat).reduce((a, i) => Math.min(a, i)),
lng: inp.map((i) => i.origin.lng).reduce((a, i) => Math.min(a, i)),
},
extent: {
lat: inp.map((i) => i.extent.lat).reduce((a, i) => Math.max(a, i)),
lng: inp.map((i) => i.extent.lng).reduce((a, i) => Math.max(a, i)),
},
};
}
return rval;
}
static roundLocation(r, roundDigits) {
return {
lat: NumberRatchet.safeNumber(r.lat.toFixed(roundDigits)),
lng: NumberRatchet.safeNumber(r.lng.toFixed(roundDigits)),
};
}
static locationToBounds(loc, radiusMiles) {
const offset = GeolocationRatchet.milesInDegLatLng(radiusMiles, loc.lat);
const gfb = {
origin: {
lat: loc.lat - offset,
lng: loc.lng - offset,
},
extent: {
lat: loc.lat + offset,
lng: loc.lng + offset,
},
};
return gfb;
}
static sameLocation(loc1, loc2) {
return !!loc1 && !!loc2 && loc1.lat === loc2.lat && loc1.lng === loc2.lng;
}
static pointInBounds(pt, bound) {
return (!!pt &&
!!bound &&
NumberRatchet.between(pt.lat, bound.origin.lat, bound.extent.lat) &&
NumberRatchet.between(pt.lng, bound.origin.lng, bound.extent.lng));
}
static pointInAnyBound(pt, inBounds, minPointsBeforeMapping = 10) {
let rval = false;
if (inBounds.length > minPointsBeforeMapping) {
const mp = GeolocationRatchet.buildRatchetLocationBoundsMap(inBounds);
rval = GeolocationRatchet.pointInRatchetLocationBoundsMap(pt, mp);
}
else {
for (let i = 0; i < inBounds.length && !rval; i++) {
rval = GeolocationRatchet.pointInBounds(pt, inBounds[i]);
}
}
return rval;
}
static pointInRatchetLocationBoundsMap(pt, mp) {
let rval = false;
const entry = GeolocationRatchet.findRatchetLocationBoundsMapEntry(pt, mp);
if (entry) {
const bounds = entry.bounds;
for (let i = 0; i < bounds.length && !rval; i++) {
rval = GeolocationRatchet.pointInBounds(pt, bounds[i]);
}
}
return rval;
}
static findRatchetLocationBoundsMapEntry(pt, mp) {
let rval = null;
if (pt.lat >= mp.latOffset && pt.lat <= mp.maxLat && pt.lng >= mp.lngOffset && pt.lng <= mp.maxLng) {
const ltIdx = Math.trunc(pt.lat) - mp.latOffset;
const lngIdx = Math.trunc(pt.lng) - mp.lngOffset;
rval = mp.mapping[ltIdx][lngIdx];
}
return rval;
}
static buildRatchetLocationBoundsMap(inBounds) {
const minLat = inBounds.map((i) => i.origin.lat).reduce((a, i) => Math.min(a, i));
const minLng = inBounds.map((i) => i.origin.lng).reduce((a, i) => Math.min(a, i));
const maxLat = inBounds.map((i) => i.extent.lat).reduce((a, i) => Math.max(a, i));
const maxLng = inBounds.map((i) => i.extent.lng).reduce((a, i) => Math.max(a, i));
const latOffset = Math.trunc(minLat) - 1;
const lngOffset = Math.trunc(minLng) - 1;
const latEntries = Math.trunc(maxLat) - latOffset + 1;
const lngEntries = Math.trunc(maxLng) - lngOffset + 1;
const mapping = [];
for (let i = 0; i < latEntries; i++) {
const newRow = [];
for (let j = 0; j < lngEntries; j++) {
newRow.push({
lat: latOffset + i,
lng: lngOffset + j,
bounds: [],
});
}
mapping.push(newRow);
}
inBounds.forEach((b) => {
for (let i = Math.trunc(b.origin.lat); i <= Math.trunc(b.extent.lat); i++) {
const latIdx = i - latOffset;
const row = mapping[latIdx];
for (let j = Math.trunc(b.origin.lng); j <= Math.trunc(b.extent.lng); j++) {
const lngIdx = j - lngOffset;
row[lngIdx].bounds.push(b);
}
}
});
return {
latOffset: latOffset,
lngOffset: lngOffset,
maxLat: latOffset + latEntries,
maxLng: lngOffset + lngEntries,
mapping: mapping,
};
}
}
//# sourceMappingURL=geolocation-ratchet.js.map