UNPKG

brahma-muhurat

Version:

High-precision Brahma Muhurat calculator for JavaScript and TypeScript

460 lines (396 loc) 13.1 kB
/** * Geographic utility functions for coordinate validation and calculations */ const moment = require('moment-timezone'); const { LatLon } = require('geodesy/latlon-spherical'); const _unusedLatLon = LatLon; // Referenced to avoid ESLint unused var warning const geolib = require('geolib'); /** * Validate geographic coordinates * @param {number} latitude - Latitude in degrees * @param {number} longitude - Longitude in degrees * @throws {Error} If coordinates are invalid */ function validateCoordinates(latitude, longitude) { if (typeof latitude !== 'number' || typeof longitude !== 'number') { throw new Error('Latitude and longitude must be numbers'); } if (latitude < -90 || latitude > 90) { throw new Error('Latitude must be between -90 and 90 degrees'); } if (longitude < -180 || longitude > 180) { throw new Error('Longitude must be between -180 and 180 degrees'); } if (isNaN(latitude) || isNaN(longitude)) { throw new Error('Latitude and longitude must be valid numbers'); } } /** * Validate timezone * @param {string} timezone - Timezone name * @throws {Error} If timezone is invalid */ function validateTimezone(timezone) { if (!timezone || typeof timezone !== 'string') { throw new Error('Timezone must be a non-empty string'); } if (!moment.tz.zone(timezone)) { throw new Error(`Invalid timezone: ${timezone}`); } } /** * Validate elevation * @param {number} elevation - Elevation in meters * @throws {Error} If elevation is invalid */ function validateElevation(elevation) { if (typeof elevation !== 'number') { throw new Error('Elevation must be a number'); } if (elevation < -500 || elevation > 9000) { throw new Error('Elevation must be between -500 and 9000 meters'); } if (isNaN(elevation)) { throw new Error('Elevation must be a valid number'); } } /** * Convert coordinates to different formats * @param {number} latitude - Latitude in decimal degrees * @param {number} longitude - Longitude in decimal degrees * @returns {Object} Coordinates in various formats */ function convertCoordinateFormats(latitude, longitude) { validateCoordinates(latitude, longitude); // Convert to degrees, minutes, seconds const latDMS = decimalToDMS(latitude, 'latitude'); const lonDMS = decimalToDMS(longitude, 'longitude'); // Convert to degrees and decimal minutes const latDM = decimalToDM(latitude, 'latitude'); const lonDM = decimalToDM(longitude, 'longitude'); return { decimal: { latitude: latitude, longitude: longitude }, dms: { latitude: latDMS, longitude: lonDMS }, dm: { latitude: latDM, longitude: lonDM }, formatted: { decimal: `${latitude.toFixed(6)}°, ${longitude.toFixed(6)}°`, dms: `${latDMS}, ${lonDMS}`, dm: `${latDM}, ${lonDM}` } }; } /** * Convert decimal degrees to degrees, minutes, seconds * @param {number} decimal - Decimal degrees * @param {string} type - 'latitude' or 'longitude' * @returns {string} DMS format string */ function decimalToDMS(decimal, type) { const direction = getDirection(decimal, type); const abs = Math.abs(decimal); const degrees = Math.floor(abs); const minutesFloat = (abs - degrees) * 60; const minutes = Math.floor(minutesFloat); const seconds = (minutesFloat - minutes) * 60; return `${degrees}° ${minutes}' ${seconds.toFixed(2)}" ${direction}`; } /** * Convert decimal degrees to degrees and decimal minutes * @param {number} decimal - Decimal degrees * @param {string} type - 'latitude' or 'longitude' * @returns {string} DM format string */ function decimalToDM(decimal, type) { const direction = getDirection(decimal, type); const abs = Math.abs(decimal); const degrees = Math.floor(abs); const minutes = (abs - degrees) * 60; return `${degrees}° ${minutes.toFixed(4)}' ${direction}`; } /** * Get direction letter for coordinate * @param {number} decimal - Decimal degrees * @param {string} type - 'latitude' or 'longitude' * @returns {string} Direction letter (N, S, E, W) */ function getDirection(decimal, type) { if (type === 'latitude') { return decimal >= 0 ? 'N' : 'S'; } else { return decimal >= 0 ? 'E' : 'W'; } } /** * Calculate distance between two coordinates * @param {number} lat1 - First latitude * @param {number} lon1 - First longitude * @param {number} lat2 - Second latitude * @param {number} lon2 - Second longitude * @returns {Object} Distance in various units */ function calculateDistance(lat1, lon1, lat2, lon2) { validateCoordinates(lat1, lon1); validateCoordinates(lat2, lon2); const distanceMeters = geolib.getDistance( { latitude: lat1, longitude: lon1 }, { latitude: lat2, longitude: lon2 } ); return { meters: distanceMeters, kilometers: distanceMeters / 1000, miles: distanceMeters / 1609.344, nauticalMiles: distanceMeters / 1852, formatted: { km: `${(distanceMeters / 1000).toFixed(2)} km`, miles: `${(distanceMeters / 1609.344).toFixed(2)} miles` } }; } /** * Calculate bearing between two coordinates * @param {number} lat1 - First latitude * @param {number} lon1 - First longitude * @param {number} lat2 - Second latitude * @param {number} lon2 - Second longitude * @returns {Object} Bearing information */ function calculateBearing(lat1, lon1, lat2, lon2) { validateCoordinates(lat1, lon1); validateCoordinates(lat2, lon2); const bearing = geolib.getBearing( { latitude: lat1, longitude: lon1 }, { latitude: lat2, longitude: lon2 } ); const compassDirection = getCompassDirection(bearing); return { degrees: bearing, compass: compassDirection, formatted: `${bearing}° (${compassDirection})` }; } /** * Get compass direction from bearing degrees * @param {number} bearing - Bearing in degrees * @returns {string} Compass direction */ function getCompassDirection(bearing) { const directions = [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ]; const index = Math.round(bearing / 22.5) % 16; return directions[index]; } /** * Check if coordinates are in polar regions * @param {number} latitude - Latitude in degrees * @returns {Object} Polar region information */ function checkPolarRegion(latitude) { validateCoordinates(latitude, 0); // Longitude doesn't matter for polar check const absLat = Math.abs(latitude); if (absLat >= 66.5) { return { isPolar: true, region: latitude > 0 ? 'Arctic' : 'Antarctic', circle: latitude > 0 ? 'Arctic Circle' : 'Antarctic Circle', warning: 'Sunrise/sunset calculations may be unreliable in polar regions during certain seasons' }; } return { isPolar: false, region: null, circle: null, warning: null }; } /** * Get timezone suggestions for coordinates * @param {number} latitude - Latitude in degrees * @param {number} longitude - Longitude in degrees * @returns {Array<string>} Suggested timezones */ function getTimezoneSuggestions(latitude, longitude) { validateCoordinates(latitude, longitude); // Simple timezone estimation based on longitude const offsetHours = Math.round(longitude / 15); const offsetMinutes = offsetHours * 60; // Get all timezones and filter by approximate offset const allTimezones = moment.tz.names(); const suggestions = []; // Current time for offset calculation const now = moment(); for (const tz of allTimezones) { const tzOffset = moment.tz(now, tz).utcOffset(); // Allow 30-minute difference for timezone matching if (Math.abs(tzOffset - offsetMinutes) <= 30) { suggestions.push(tz); } } // Prioritize common timezones const priorityTimezones = [ 'Asia/Kolkata', 'Asia/Dubai', 'Europe/London', 'America/New_York', 'America/Los_Angeles', 'Australia/Sydney', 'Asia/Tokyo' ]; const prioritySuggestions = suggestions.filter(tz => priorityTimezones.includes(tz) ); const otherSuggestions = suggestions.filter(tz => !priorityTimezones.includes(tz) ); return [...prioritySuggestions, ...otherSuggestions.slice(0, 5)]; } /** * Get country information from coordinates (simplified) * @param {number} latitude - Latitude in degrees * @param {number} longitude - Longitude in degrees * @returns {Object} Country information */ function getCountryInfo(latitude, longitude) { validateCoordinates(latitude, longitude); // Simplified country detection for major countries // In a real implementation, you would use a reverse geocoding service if (latitude >= 6 && latitude <= 37 && longitude >= 68 && longitude <= 97) { return { country: 'India', code: 'IN', suggestedTimezone: 'Asia/Kolkata', currency: 'INR' }; } if (latitude >= 25 && latitude <= 49 && longitude >= -125 && longitude <= -66) { return { country: 'United States', code: 'US', suggestedTimezone: 'America/New_York', currency: 'USD' }; } if (latitude >= 49 && latitude <= 61 && longitude >= -141 && longitude <= -52) { return { country: 'Canada', code: 'CA', suggestedTimezone: 'America/Toronto', currency: 'CAD' }; } return { country: 'Unknown', code: null, suggestedTimezone: null, currency: null }; } /** * Calculate magnetic declination (simplified) * @param {number} latitude - Latitude in degrees * @param {number} longitude - Longitude in degrees * @param {Date} date - Date for calculation * @returns {number} Magnetic declination in degrees */ function calculateMagneticDeclination(latitude, longitude, date) { validateCoordinates(latitude, longitude); // Simplified magnetic declination calculation // In a real implementation, you would use the World Magnetic Model (WMM) const year = date.getFullYear(); const yearFraction = (date.getTime() - new Date(year, 0, 1).getTime()) / (new Date(year + 1, 0, 1).getTime() - new Date(year, 0, 1).getTime()); const decimalYear = year + yearFraction; // Very simplified model - real declination varies significantly const declination = Math.sin(latitude * Math.PI / 180) * Math.cos(longitude * Math.PI / 180) * (decimalYear - 2020) * 0.1; return declination; } /** * Get elevation zone information * @param {number} elevation - Elevation in meters * @returns {Object} Elevation zone information */ function getElevationZone(elevation) { if (elevation !== undefined) { validateElevation(elevation); } if (elevation === undefined || elevation === null) { return { zone: 'Unknown', description: 'Elevation not specified', pressureEstimate: 1013.25 }; } let zone, description, pressureEstimate; if (elevation < 0) { zone = 'Below Sea Level'; description = 'Below sea level - may affect atmospheric calculations'; pressureEstimate = 1013.25 + Math.abs(elevation) * 0.012; } else if (elevation < 500) { zone = 'Low Elevation'; description = 'Near sea level - minimal atmospheric effects'; pressureEstimate = 1013.25 - elevation * 0.012; } else if (elevation < 1500) { zone = 'Moderate Elevation'; description = 'Moderate elevation - some atmospheric effects'; pressureEstimate = 1013.25 - elevation * 0.012; } else if (elevation < 3000) { zone = 'High Elevation'; description = 'High elevation - significant atmospheric effects'; pressureEstimate = 1013.25 - elevation * 0.012; } else { zone = 'Very High Elevation'; description = 'Very high elevation - major atmospheric corrections needed'; pressureEstimate = 1013.25 - elevation * 0.012; } return { zone, description, pressureEstimate: Math.max(pressureEstimate, 300), // Minimum realistic pressure elevationFeet: elevation * 3.28084 }; } /** * Format coordinates for display * @param {number} latitude - Latitude in degrees * @param {number} longitude - Longitude in degrees * @param {string} format - Format type ('decimal', 'dms', 'dm') * @returns {string} Formatted coordinates */ function formatCoordinates(latitude, longitude, format = 'decimal') { validateCoordinates(latitude, longitude); const formats = convertCoordinateFormats(latitude, longitude); switch (format) { case 'dms': return formats.formatted.dms; case 'dm': return formats.formatted.dm; case 'decimal': default: return formats.formatted.decimal; } } module.exports = { validateCoordinates, validateTimezone, validateElevation, convertCoordinateFormats, calculateDistance, calculateBearing, checkPolarRegion, getTimezoneSuggestions, getCountryInfo, calculateMagneticDeclination, getElevationZone, formatCoordinates, decimalToDMS, decimalToDM, getDirection, getCompassDirection };