pulse-ai-utils
Version:
Utility functions and helpers for AI-powered applications
207 lines • 8.17 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.GeohashService = void 0;
const geohash = __importStar(require("ngeohash"));
class GeohashService {
constructor() {
this.DEFAULT_PRECISION = 6; // ~0.61km x 0.61km cells
}
/**
* Generate geohash for coordinates
*/
generateGeohash(lat, lng, precision = this.DEFAULT_PRECISION) {
return geohash.encode(lat, lng, precision);
}
/**
* Get neighboring geohashes for a given geohash
* This is crucial for boundary searches
*/
getNeighbors(hash) {
return [
hash,
geohash.neighbor(hash, [1, 0]), // North
geohash.neighbor(hash, [1, 1]), // Northeast
geohash.neighbor(hash, [0, 1]), // East
geohash.neighbor(hash, [-1, 1]), // Southeast
geohash.neighbor(hash, [-1, 0]), // South
geohash.neighbor(hash, [-1, -1]), // Southwest
geohash.neighbor(hash, [0, -1]), // West
geohash.neighbor(hash, [1, -1]) // Northwest
];
}
/**
* Calculate geohash search ranges for a radius query
* This minimizes the number of queries needed
*/
getSearchRanges(lat, lng, radius) {
// Determine optimal geohash precision based on radius
const precision = this.getOptimalPrecision(radius);
const centerHash = geohash.encode(lat, lng, precision);
// Get all geohashes that need to be searched
const searchHashes = this.getSearchHashes(centerHash, radius, precision);
// Convert to ranges for efficient querying
return this.hashesToRanges(searchHashes);
}
/**
* Get optimal geohash precision for given radius
*/
getOptimalPrecision(radius) {
// Precision to cell size mapping (approximate)
const precisionMap = [
{ precision: 1, km: 5000 },
{ precision: 2, km: 1250 },
{ precision: 3, km: 156 },
{ precision: 4, km: 39 },
{ precision: 5, km: 4.9 },
{ precision: 6, km: 1.2 },
{ precision: 7, km: 0.152 },
{ precision: 8, km: 0.038 }
];
// Find precision where cell size is about 1/4 of search radius
for (let i = precisionMap.length - 1; i >= 0; i--) {
if (precisionMap[i].km <= radius / 4) {
return precisionMap[i].precision;
}
}
return 6; // Default precision
}
/**
* Get all geohash cells that overlap with search radius
*/
getSearchHashes(centerHash, radius, precision) {
const hashes = new Set();
const center = geohash.decode(centerHash);
const centerLat = center.latitude;
const centerLng = center.longitude;
// Get cell dimensions at this precision
const cellSize = this.getCellSize(precision);
// Calculate how many cells we need to check in each direction
const cellsToCheck = Math.ceil(radius / cellSize.km);
// Start with center and expand outward
const queue = [{ hash: centerHash, distance: 0 }];
const visited = new Set();
while (queue.length > 0) {
const { hash, distance } = queue.shift();
if (visited.has(hash) || distance > cellsToCheck)
continue;
visited.add(hash);
// Check if this cell is within radius
const cell = geohash.decode(hash);
const cellLat = cell.latitude;
const cellLng = cell.longitude;
const cellDistance = this.haversineDistance(centerLat, centerLng, cellLat, cellLng);
if (cellDistance <= radius + cellSize.km) {
hashes.add(hash);
// Add neighbors to queue
const neighbors = this.getNeighbors(hash);
neighbors.forEach(n => {
if (!visited.has(n)) {
queue.push({ hash: n, distance: distance + 1 });
}
});
}
}
return hashes;
}
/**
* Convert set of geohashes to efficient query ranges
*/
hashesToRanges(hashes) {
// Sort hashes
const sortedHashes = Array.from(hashes).sort();
const ranges = [];
let rangeStart = sortedHashes[0];
let rangeEnd = sortedHashes[0];
for (let i = 1; i < sortedHashes.length; i++) {
const current = sortedHashes[i];
const prev = sortedHashes[i - 1];
// Check if current is consecutive to previous
if (this.areConsecutive(prev, current)) {
rangeEnd = current;
}
else {
// Save current range and start new one
ranges.push([rangeStart, rangeEnd + '~']); // ~ is after z in ASCII
rangeStart = current;
rangeEnd = current;
}
}
// Add final range
ranges.push([rangeStart, rangeEnd + '~']);
return ranges;
}
/**
* Check if two geohashes are consecutive
*/
areConsecutive(hash1, hash2) {
// Simple check - can be improved
const base32 = '0123456789bcdefghjkmnpqrstuvwxyz';
const idx1 = base32.indexOf(hash1[hash1.length - 1]);
const idx2 = base32.indexOf(hash2[hash2.length - 1]);
return hash1.slice(0, -1) === hash2.slice(0, -1) && idx2 - idx1 === 1;
}
/**
* Calculate distance between two points
*/
haversineDistance(lat1, lng1, lat2, lng2) {
const R = 6371; // Earth's radius in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* Get cell size for precision level
*/
getCellSize(precision) {
const sizes = {
1: { km: 5000, lat: 45, lng: 45 },
2: { km: 1250, lat: 11.25, lng: 11.25 },
3: { km: 156, lat: 1.40625, lng: 2.8125 },
4: { km: 39, lat: 0.3515625, lng: 0.3515625 },
5: { km: 4.9, lat: 0.0439453125, lng: 0.087890625 },
6: { km: 1.2, lat: 0.010986328125, lng: 0.010986328125 },
7: { km: 0.152, lat: 0.001373291015625, lng: 0.00274658203125 },
8: { km: 0.038, lat: 0.00034332275390625, lng: 0.00034332275390625 }
};
return sizes[precision] || sizes[6];
}
}
exports.GeohashService = GeohashService;
//# sourceMappingURL=geohash-service.js.map