const { sequelize } = require("../models"); const { QueryTypes } = require("sequelize"); const logger = require("../utils/logger"); class LocationService { /** * Find users within a specified radius of coordinates * Uses the Haversine formula to calculate great-circle distance between two points * * @param {number} latitude - Center point latitude * @param {number} longitude - Center point longitude * @param {number} radiusMiles - Search radius in miles (default: 10) * @returns {Promise} Array of users with their distance from the center point */ async findUsersInRadius(latitude, longitude, radiusMiles = 10) { if (!latitude || !longitude) { throw new Error("Latitude and longitude are required"); } if (radiusMiles <= 0 || radiusMiles > 100) { throw new Error("Radius must be between 1 and 100 miles"); } try { // Haversine formula: // distance = 3959 * acos(cos(radians(lat1)) * cos(radians(lat2)) // * cos(radians(lng2) - radians(lng1)) // + sin(radians(lat1)) * sin(radians(lat2))) // Note: 3959 is Earth's radius in miles const query = ` SELECT * FROM ( SELECT u.id, u.email, u."firstName", u."lastName", ua.latitude, ua.longitude, (3959 * acos( LEAST(1.0, cos(radians(:lat)) * cos(radians(ua.latitude)) * cos(radians(ua.longitude) - radians(:lng)) + sin(radians(:lat)) * sin(radians(ua.latitude)) ) )) AS distance FROM "Users" u INNER JOIN "UserAddresses" ua ON u.id = ua."userId" WHERE ua."isPrimary" = true AND ua.latitude IS NOT NULL AND ua.longitude IS NOT NULL ) AS user_distances WHERE distance < :radiusMiles ORDER BY distance ASC `; const users = await sequelize.query(query, { replacements: { lat: parseFloat(latitude), lng: parseFloat(longitude), radiusMiles: parseFloat(radiusMiles), }, type: QueryTypes.SELECT, }); return users.map((user) => ({ id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, latitude: parseFloat(user.latitude), longitude: parseFloat(user.longitude), distance: parseFloat(user.distance).toFixed(2), // Round to 2 decimal places })); } catch (error) { logger.error("Error finding users in radius", { error }); throw new Error(`Failed to find users in radius: ${error.message}`); } } /** * Calculate distance between two points using Haversine formula * * @param {number} lat1 - First point latitude * @param {number} lon1 - First point longitude * @param {number} lat2 - Second point latitude * @param {number} lon2 - Second point longitude * @returns {number} Distance in miles */ calculateDistance(lat1, lon1, lat2, lon2) { const R = 3959; // Earth's radius in miles const dLat = this.toRadians(lat2 - lat1); const dLon = this.toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const distance = R * c; return distance; } /** * Convert degrees to radians * @param {number} degrees * @returns {number} Radians */ toRadians(degrees) { return degrees * (Math.PI / 180); } } module.exports = new LocationService();