""" Geolocation Verification Module for Attendr Implements KR Rule #2: Geolocation Verification ∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)] """ from utils import haversine_distance, validate_coordinates from models import Classroom, db from config import Config import logging # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class GeolocationModule: """Handles all geolocation verification operations""" def __init__(self, default_radius=None): """ Initialize geolocation module Args: default_radius: Default radius in meters for geofencing """ self.default_radius = default_radius or Config.GEOLOCATION_RADIUS_METERS logger.info(f"Geolocation Module initialized with default radius={self.default_radius}m") def get_classroom_location(self, classroom_id): """ Get classroom GPS coordinates and radius Args: classroom_id: ID of the classroom Returns: tuple: (latitude, longitude, radius) or (None, None, None) if not found """ try: classroom = Classroom.query.get(classroom_id) if not classroom: logger.warning(f"Classroom {classroom_id} not found") return None, None, None radius = classroom.radius_meters or self.default_radius logger.info(f"Classroom {classroom.name}: lat={classroom.latitude}, lon={classroom.longitude}, radius={radius}m") return classroom.latitude, classroom.longitude, radius except Exception as e: logger.error(f"Error fetching classroom location: {str(e)}") return None, None, None def calculate_distance(self, lat1, lon1, lat2, lon2): """ Calculate distance between two GPS coordinates Args: lat1, lon1: First coordinate lat2, lon2: Second coordinate Returns: Distance in meters """ try: distance = haversine_distance(lat1, lon1, lat2, lon2) logger.info(f"Distance calculated: {distance:.2f} meters") return distance except Exception as e: logger.error(f"Distance calculation error: {str(e)}") return None def is_within_allowed_area(self, student_lat, student_lon, classroom_id): """ Check if student is within allowed classroom area Implements: IsWithinAllowedArea(x) Args: student_lat: Student's latitude student_lon: Student's longitude classroom_id: ID of the classroom Returns: tuple: (is_within: bool, distance: float, error_message: str or None) """ try: # Validate student coordinates valid, error = validate_coordinates(student_lat, student_lon) if not valid: logger.warning(f"Invalid student coordinates: {error}") return False, None, error # Get classroom location class_lat, class_lon, radius = self.get_classroom_location(classroom_id) if class_lat is None: return False, None, "Classroom location not found" # Calculate distance distance = self.calculate_distance( float(student_lat), float(student_lon), class_lat, class_lon ) if distance is None: return False, None, "Failed to calculate distance" # Check if within radius is_within = distance <= radius logger.info(f"Location verification: within_area={is_within}, distance={distance:.2f}m, allowed_radius={radius}m") if is_within: return True, distance, None else: return False, distance, f"You are {distance:.0f}m away from the classroom (max allowed: {radius}m)" except Exception as e: logger.error(f"Location verification error: {str(e)}") return False, None, f"Location verification failed: {str(e)}" def verify_location(self, student_lat, student_lon, classroom_id): """ Complete location verification process Implements: Student(x) ∧ IsWithinAllowedArea(x) → VerifiedLocation(x) Args: student_lat: Student's latitude student_lon: Student's longitude classroom_id: ID of the classroom Returns: dict: Verification result with status, distance, and message """ is_within, distance, error = self.is_within_allowed_area( student_lat, student_lon, classroom_id ) result = { 'verified': is_within, 'distance': distance, 'message': None, 'error': error } if is_within: result['message'] = f"Location verified! You are {distance:.0f}m from the classroom." elif error: result['message'] = error return result def get_all_classrooms(self): """ Get all available classrooms Returns: List of classroom dictionaries """ try: classrooms = Classroom.query.all() return [classroom.to_dict() for classroom in classrooms] except Exception as e: logger.error(f"Error fetching classrooms: {str(e)}") return [] def add_classroom(self, name, building, latitude, longitude, radius_meters=None): """ Add a new classroom to the system Args: name: Classroom name building: Building name latitude: GPS latitude longitude: GPS longitude radius_meters: Geofencing radius (optional) Returns: tuple: (success, classroom_dict or error_message) """ try: # Validate coordinates valid, error = validate_coordinates(latitude, longitude) if not valid: return False, error # Create classroom classroom = Classroom( name=name, building=building, latitude=float(latitude), longitude=float(longitude), radius_meters=radius_meters or self.default_radius ) db.session.add(classroom) db.session.commit() logger.info(f"Classroom added: {name} in {building}") return True, classroom.to_dict() except Exception as e: db.session.rollback() logger.error(f"Error adding classroom: {str(e)}") return False, f"Failed to add classroom: {str(e)}" # Singleton instance geolocation_module = GeolocationModule()