/// Qibla direction utilities. Pure math, zero external dependencies. /// /// Computes the initial bearing (forward azimuth) from any point on Earth to /// the Ka'bah using the spherical law of cosines. Includes compass direction /// lookup, great-circle interpolation, and haversine distance. /// /// Ka'bah coordinates sourced from verified GPS surveys. /// /// Example: /// ```dart /// import 'package:qibla/qibla.dart'; /// /// final bearing = qiblaAngle(40.7128, -74.006); // ~58.48 /// print(compassDir(bearing)); // NE /// ``` library; import 'dart:math'; /// Latitude of the Ka'bah center, Masjid al-Haram, Mecca (degrees north). const double kaabaLat = 21.422511; /// Longitude of the Ka'bah center, Masjid al-Haram, Mecca (degrees east). const double kaabaLng = 39.82615; /// Mean radius of the Earth in kilometers (WGS-84 volumetric mean). const double earthRadiusKm = 6371; const double _deg = pi / 180; /// Eight-point compass abbreviations. const List _compassAbbr = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']; /// Eight-point compass full names. const List _compassNames = [ 'North', 'Northeast', 'East', 'Southeast', 'South', 'Southwest', 'West', 'Northwest', ]; /// Qibla bearing in degrees clockwise from true north. /// /// Uses the forward azimuth formula from spherical trigonometry. /// Result range: [0, 360). /// /// [lat] is the observer latitude in decimal degrees (-90 to 90). /// [lng] is the observer longitude in decimal degrees (-180 to 180). /// /// Returns the bearing in degrees clockwise from north /// (0 = N, 90 = E, 180 = S, 270 = W). /// /// Throws [RangeError] if latitude is outside [-90, 90] or longitude /// outside [-180, 180]. /// /// Example: /// ```dart /// final bearing = qiblaAngle(40.7128, -74.006); // ~58.48 (northeast) /// ``` double qiblaAngle(double lat, double lng) { if (lat < -90 || lat > 90) { throw RangeError('Latitude must be between -90 and 90, got $lat'); } if (lng < -180 || lng > 180) { throw RangeError('Longitude must be between -180 and 180, got $lng'); } final phi1 = lat * _deg; final lam1 = lng * _deg; final phi2 = kaabaLat * _deg; final lam2 = kaabaLng * _deg; final y = sin(lam2 - lam1) * cos(phi2); final x = cos(phi1) * sin(phi2) - sin(phi1) * cos(phi2) * cos(lam2 - lam1); return (atan2(y, x) / _deg + 360) % 360; } /// Eight-point compass abbreviation for a bearing. /// /// [bearing] is the bearing in degrees (0-360). /// /// Returns a compass abbreviation: N, NE, E, SE, S, SW, W, or NW. /// /// Example: /// ```dart /// compassDir(58.5); // 'NE' /// compassDir(180.0); // 'S' /// ``` String compassDir(double bearing) { return _compassAbbr[(bearing / 45).round() % 8]; } /// Full compass direction name for a bearing. /// /// [bearing] is the bearing in degrees (0-360). /// /// Returns the full direction name (North, Northeast, East, Southeast, /// South, Southwest, West, or Northwest). /// /// Example: /// ```dart /// compassName(58.5); // 'Northeast' /// compassName(270.0); // 'West' /// ``` String compassName(double bearing) { return _compassNames[(bearing / 45).round() % 8]; } /// Great-circle waypoints from ([lat], [lng]) to the Ka'bah. /// /// Uses Slerp (spherical linear interpolation). Useful for drawing Qibla /// direction lines on maps. /// /// [lat] is the origin latitude in decimal degrees. /// [lng] is the origin longitude in decimal degrees. /// [steps] is the number of segments (default: 120, producing 121 points). /// /// Returns a list of `[latitude, longitude]` pairs in decimal degrees. /// The first element is the observer location; the last is the Ka'bah. /// /// Throws [RangeError] if coordinates are out of bounds. /// /// Example: /// ```dart /// final path = qiblaGreatCircle(51.5074, -0.1278); // 121 points London -> Makkah /// print(path.length); // 121 /// print(path.first); // [51.5074, -0.1278] /// ``` List> qiblaGreatCircle(double lat, double lng, [int steps = 120]) { if (lat < -90 || lat > 90) { throw RangeError('Latitude must be between -90 and 90, got $lat'); } if (lng < -180 || lng > 180) { throw RangeError('Longitude must be between -180 and 180, got $lng'); } final phi1 = lat * _deg; final lam1 = lng * _deg; final phi2 = kaabaLat * _deg; final lam2 = kaabaLng * _deg; final d = 2 * asin( sqrt( pow(sin((phi2 - phi1) / 2), 2) + cos(phi1) * cos(phi2) * pow(sin((lam2 - lam1) / 2), 2), ), ); if (d == 0) { return [ [lat, lng], ]; } final points = >[]; for (var i = 0; i <= steps; i++) { final f = i / steps; final a = sin((1 - f) * d) / sin(d); final b = sin(f * d) / sin(d); final x = a * cos(phi1) * cos(lam1) + b * cos(phi2) * cos(lam2); final y = a * cos(phi1) * sin(lam1) + b * cos(phi2) * sin(lam2); final z = a * sin(phi1) + b * sin(phi2); points.add([atan2(z, sqrt(x * x + y * y)) / _deg, atan2(y, x) / _deg]); } return points; } /// Haversine distance between two coordinate pairs. /// /// [lat1], [lng1] define the first point in decimal degrees. /// [lat2], [lng2] define the second point in decimal degrees. /// /// Returns the distance in kilometers (spherical Earth approximation, /// R = 6,371 km). /// /// Example: /// ```dart /// // Distance from New York to the Ka'bah /// final km = distanceKm(40.7128, -74.006, kaabaLat, kaabaLng); // ~9634 /// ``` double distanceKm(double lat1, double lng1, double lat2, double lng2) { final dLat = (lat2 - lat1) * _deg; final dLng = (lng2 - lng1) * _deg; final a = pow(sin(dLat / 2), 2) + cos(lat1 * _deg) * cos(lat2 * _deg) * pow(sin(dLng / 2), 2); return earthRadiusKm * 2 * atan2(sqrt(a), sqrt(1 - a)); }