UNPKG

@bitblit/ratchet-common

Version:

Common tools for general use

264 lines 10.8 kB
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